Conversation
Pi (`~/.pi/agent/models.json`) and its fork oh-my-pi (`~/.omp/agent/models.yml`) can now be pointed at the gateway in one step. Both get a provider of their own (`thinkwatch`), never a built-in one, so `/login` credentials are not sent to the gateway. The provider carries the gateway's model list for the client's key; each model uses its family's API (Claude: anthropic-messages, Gemini: google-generative-ai, GPT/o-series/Codex: openai-responses, others: openai-completions) with the base URL that API expects. The default model is left alone, like opencode. oh-my-pi always gets `auth: apiKey`, otherwise its custom anthropic-messages models are shaped as Claude Code. Pi's key is escaped so `$` and a leading `!` are taken literally. When HTTP(S)_PROXY or Pi's httpProxy setting applies and NO_PROXY does not list the gateway host, the plan says Pi will send gateway requests through the proxy. Both MCP files are listed read-only and scanned along with skills, prompts/commands and instruction files. `~/.agents/skills` (and a project's `.agents/skills`) is attributed to a shared folder instead of DeepSeek Harness / Antigravity CLI. The DeepSeek Harness desktop app counts as an installed DSH, and the DSH plan notes that models used through a DeepSeek account in the desktop app go straight to api.deepseek.com. Codex no longer lists OPENAI_BASE_URL, which it stopped reading in openai/codex 4b8bab6. YAML writing can now replace a whole value (via tw_yaml::put) and write a nested key into an empty file, which oh-my-pi's model list needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Pi and oh-my-pi are now clients that can be pointed at the gateway in one step. Pi (earendil-works/pi, formerly badlogic/pi-mono) gets a provider in
~/.pi/agent/models.json; its fork oh-my-pi (can1357/oh-my-pi,omp) gets the same provider in~/.omp/agent/models.yml(ormodels.yamlwhen that is the file omp reads). The plan dialog shows the full diff, the file is backed up, and restore removes exactly what was written (a file created here is deleted again). Both are markedfields_only: the field names are checked against the source, nothing was run on this machine.Pi,
~/.pi/agent/models.json(merged into the existing file; the record goes into themodels.json.thinkwatch.jsonsidecar because the file is JSON):{ "providers": { "thinkwatch": { "name": "ThinkWatch", "baseUrl": "http://127.0.0.1:8788/v1", "api": "openai-completions", "apiKey": "tw-…", "models": [ { "id": "claude-sonnet-5", "api": "anthropic-messages", "baseUrl": "http://127.0.0.1:8788" }, { "id": "gpt-5.5", "api": "openai-responses" }, { "id": "gemini-3-pro", "api": "google-generative-ai", "baseUrl": "http://127.0.0.1:8788/v1beta" }, { "id": "deepseek-chat" } ] } } }oh-my-pi,
~/.omp/agent/models.yml(sentinel comment block at the top, as for the other YAML clients):thinkwatch). Both clients keep credentials per provider, and/loginstores them under built-in ids (anthropic,openai-codex, …). Rewriting a built-in provider'sbaseUrlwould send those credentials to the gateway.writes_models): the gateway'sGET /v1/modelsfor the client's key. Each model uses its family's API so a same-format upstream is passed through unconverted: Claude →anthropic-messages(base URL without/v1), Gemini →google-generative-ai(/v1beta), GPT / o-series / Codex →openai-responses, everything else → the provider defaultopenai-completions(/v1). The Clients page flags a stale list and re-plans it through the same diff.settings.json(Pi) andconfig.yml(omp) are not touched. The plan says where the ThinkWatch models are chosen.auth, omp shapes customanthropic-messagesmodels as Claude Code (OAuth-style request shaping).auth: apiKeyis written with a key,auth: nonewithout one (omp rejects a provider with models and neither).$NAME/${NAME}and a leading!inapiKey;$is written as$$and a leading!as$!. Gateway keys (tw-+ 24 base32 characters) never contain either, so in practice the value is unchanged. omp only interprets a leading!and a value that is exactly an environment variable name, which atw-key cannot be.models.jsonwhen/modelopens, omp's model picker re-readsmodels.ymlwhen its mtime changed.takes_effect: immediately,reloads: true.models.yml/models.yaml, the first that exists. The takeover writes into that one; ifmodels.ymlappears later over amodels.yamltakeover, the plan and the diagnosis say that nothing written here is used (new messages, not the generic "a setting of the same name wins").CODEX_HOME:PI_CODING_AGENT_DIR(Pi);PI_CODING_AGENT_DIR,PI_CONFIG_DIR,OMP_PROFILE,PI_PROFILE(omp). Both clients are movable on the Clients page (layout: the agent directory).~/.pi/agent/mcp.jsonand~/.omp/agent/mcp.json(mcpServers) appear on the MCP page, read-only (adopt.mcp.unverified_format, no local sample). The scan covers both MCP files (plus omp's.mcp.jsonand itsdisabledServers/enabledServerslists),skills/, Pi'sprompts/and omp'scommands/+prompts/as slash commands, and the instruction files read into context (AGENTS.md,AGENTS.override.md,CLAUDE.md,SYSTEM.md,APPEND_SYSTEM.mdfor Pi;AGENTS.md,SYSTEM.md,RULES.mdfor omp). Project level:.pi/and.omp/MCP and skills.Caveats shown in the plan (new message codes, Chinese in
core.zh.json):adopt.cost.pi.default_model)adopt.cost.omp.default_model)adopt.plan.pi.proxy_env), or the same sentence for Pi's ownhttpProxysetting (adopt.plan.pi.proxy_setting). Pi uses undici'sEnvHttpProxyAgent, which proxies every host, 127.0.0.1 included, whenNO_PROXYis empty; the check follows undici's rules (http_proxybeforeHTTP_PROXY, an empty value means no proxy,httpsfalls back to the HTTP proxy,NO_PROXYhost/port/suffix/*matching). The proxy variables come from the login-shell environment:user_envstill keeps them away from core, but now sets them aside for this check. omp needs no note: it bypasses the proxy for loopback and private ranges.adopt.plan.fields_onlynote, andadopt.plan.no_modelswhen the key has no models.DeepSeek Harness desktop and web. Both read the home patch layer that the existing DSH takeover writes, so they were already covered (checked in 0.2.0-rc.2, see below). Added: the installed desktop app (
DeepSeek Harness.appin/Applicationsor~/Applications;%LOCALAPPDATA%\Programs\DeepSeek Harness\DeepSeek Harness.exeon Windows) marks DSH as installed even before~/.dshexists, which also makes its MCP column present. New caveat: "Models used through a DeepSeek account signed in to the desktop app go straight to api.deepseek.com and do not pass through the gateway." (adopt.cost.dsh.account_direct). The communitydsh-desktopis out of scope.Shared skills folder.
~/.agents/skillsand a project's.agents/skillsare listed under a shared folder (agents, shown as 共用目录 / Shared folder) instead of DeepSeek Harness and Antigravity CLI. Pi, oh-my-pi, DSH, agy, Copilot, Kimi, Goose, Crush, Kilo, Cline and MiMo read it.Codex.
OPENAI_BASE_URLis no longer among Codex's diagnostic env vars: openai/codex 4b8bab6 (2026-04-03, "Remove OPENAI_BASE_URL config fallback") stopped reading it, so an exported one was misreported as overriding the takeover.YAML writer.
tw_adopt::yaml::setcan now replace a whole value (a list or a map) throughtw_yaml::put(the same self-checked path core uses for config.yaml), and write a nested key into a file that has no nodes yet.getreturns a container in one-line form so its original can be recorded. Strings inside written blocks are plain only when they are unambiguous (letter first,[A-Za-z0-9-_./:@+], not a bool/null/number lookalike), otherwise quoted.What was checked in the sources
Pi at
0f8740b(coding-agent 0.99.2):~/.pi/agent, overridePI_CODING_AGENT_DIR: config.ts L539-L568,models.json/settings.jsonL577-L589models.jsonschema (providername/baseUrl/apiKey/api/models[], modelid/name/api/baseUrl/…), comments stripped before parsing: model-config.ts L188-L244, L297apiandbaseUrloverride the provider's: provider-composer.ts L192-L205apiKeyinterpretation (!command,$NAME,$$,$!): resolve-config-value.ts L28-L86--api-key,auth.json,models.jsonapiKey, env),/modelreloads the file,Ctrl+Ssaves the default: docs/models.md L23-L66; the reload in code: model-selector.ts L158, L192 → model-runtime.ts L839-L840sk-ant-oatOAuth tokens; an API key goes out asx-api-keywith Pi's own User-Agent: anthropic-messages.ts L980-L1079; Google base URL already carries the version: google-generative-ai.ts L350-L366~/.pi/agent/mcp.json/.pi/mcp.json: CHANGELOG L80-L92, docs/mcp.md L32; skills incl.~/.agents/skills: package-manager.ts L2458, L2549-L2561; agent-dir files: docs/configuration.mdoh-my-pi at
6e4ac4a(coding-agent 18.4.8):models.ymlthenmodels.yaml, only the first that exists is read; legacymodels.jsonmigration: docs/models.md L20-L33, config-file.ts L187-L195; agent dir and overrides: dirs.ts L1-L28, L594-L596authomitted → customanthropic-messagesmodels are shaped as OAuth / Claude Code: custom-models.ts L51-L63, docs/models.md L146-L148name; modelid/api/baseUrl): models-config-schema-bundle.ts L192-L345;apiKeyrequired with models unlessauth: none|oauth: models-config.ts L66-L80; key resolution (!command, exact env-var name, else literal): resolve-config-value.ts L100-L108api(per-model only fromsupported_endpoint_types, which the gateway does not send), so the list is written instead: model-registry.ts L1607-L1614/model, thedefaultrole: docs/models.md L602, L678~/.omp/agent/mcp.json,disabledServers/enabledServers: docs/mcp-config.md L18-L21, L93-L97; skills incl. theagentsprovider: docs/skills.md L91-L122DeepSeek Harness at tag
dsh-v0.2.0-rc.2(639ed01):runProfilebuilds that context withhome: resolveDshHome(): profile-boot.ts L68-L74, L287-L296; the desktop host runsrunProfile({ profile: 'desktop' }): desktop-host/src/index.ts L25-L30, under the shared$DSH_HOME/profiles/desktop: desktop/src/paths.ts L12-L21llm-deepseekrow (now@deepseek-ai/dsh-llm-deepseek-api-key) still honoursbaseURLandapiKeyEnv: llm-deepseek-api-key config.ts L14-L17, llm-deepseek config.ts L83; the account route is a separate rowllm-deepseek-account(README) whose base URL defaults tohttps://api.deepseek.com/anthropic(config.ts L106, L290) and whose token is handed out only for the allowed origin (llm-deepseek-account index.ts L17-L26, deepseek-account index.ts L95-L99)productName: 'DeepSeek Harness', macOSdmg/zip, Windows NSIS per user: electron-builder-config.mjs L109, L164, L231-L242; electron-builder's per-user default$LocalAppData\Programs\${APP_FILENAME}with the product name: multiUser.nsh L28-L47, targetUtil.ts L41-L43Pi logo: Lobe Icons
pi.svg("Pi Agent", pi.dev, MIT).How it was verified
Nothing was installed or run; no third-party client, no login.
cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check: cleancargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings: cleancargo test --manifest-path src-tauri/Cargo.toml -p tw-adopt -p tw-scan: 237 + 17 + 55 + 31 + 31 passedcargo test --manifest-path src-tauri/Cargo.toml --lib: 337 passed (plus the two crates)cargo test --manifest-path src-tauri/Cargo.toml --test msg_codes --test ts_bindings: passed (msg-codes.txtandlite-api.tsregenerated)npx tsc --noEmit,npx tsc --noEmit -p scripts/shots: clean;npx vitest run: 583 passedNew tests: byte-identical restore of a Pi
models.jsonwith comments and another provider, and of an ompmodels.ymlwith comments and another provider; adopting omp twice is a no-op; rewriting the model list keeps the first record; files created here are removed again; omp writes intomodels.yamlwhen that is the file it reads and reports a latermodels.ymlas hiding everything; the proxy note (undici precedence,NO_PROXYmatching, Pi'shttpProxy, shell-file fallback, nothing for omp); Pi key escaping checked against Pi's parsing rules; per-model API choice; stale model list flagged for Pi and omp on the Clients listing; YAML whole-value write/remove and quoting; scan sources and MCP listing for Pi and omp (incl.disabledServers), the shared skills folder; the DSH desktop app counted as installed; Codex no longer reportsOPENAI_BASE_URL.Not verified / open
fields_only). The gateway's conversions for Pi'sgoogle-generative-aiandopenai-responsesrequests are untested end to end.InstallLocationregistry value is not read). Linux has no published desktop build.models.jsonintomodels.ymlonly while neither YAML file exists; a takeover on a machine where that migration has not run yet would createmodels.ymlfirst and the oldmodels.jsonwould stay unmigrated. Not handled.scripts/shots/mockupdated); the feature docs on thinkwat.ch were not touched.🤖 Generated with Claude Code