From a65cc6b9dc2979a3ba7fa394bded0b808b3082e8 Mon Sep 17 00:00:00 2001 From: Lukin Date: Tue, 27 Oct 2026 21:57:34 +0800 Subject: [PATCH] fix: use stable GPT lane names and sort lane configuration Rename Astra, Luna, Sol, and Terra lanes without changing upstream model IDs. Preserve versioned client aliases and prepare v0.30.58. Co-Authored-By: Codex --- apps/gateway/e2e/eval.spec.ts | 4 +- apps/gateway/e2e/routing.spec.ts | 6 +- .../src/routes/execute.default-config.test.ts | 23 +- config/lanes.yaml | 387 +++++++++--------- config/model-aliases.yaml | 80 ++-- docs/04-routing-and-lanes.md | 21 +- implementation-notes.md | 15 +- package.json | 2 +- .../core/src/config/rules-routing.test.ts | 4 +- packages/core/src/config/samples.test.ts | 108 +++-- 10 files changed, 338 insertions(+), 312 deletions(-) diff --git a/apps/gateway/e2e/eval.spec.ts b/apps/gateway/e2e/eval.spec.ts index b9f18af4..35c0c028 100644 --- a/apps/gateway/e2e/eval.spec.ts +++ b/apps/gateway/e2e/eval.spec.ts @@ -48,8 +48,8 @@ const UNCERTAIN = { ...AUTH, "x-helm-rules-threshold": "0.99" }; // executes in hermetic CI. No OAuth accounts are bound in this fixture, so the // subscription aliases fail open and both balanced/premium eventually serve the // keyed DeepSeek Pro fallback through the local mock. -const BALANCED_CONFIGURED_PRIMARY = "gpt-6.1-sol"; -const PREMIUM_CONFIGURED_PRIMARY = "gpt-6-astra"; +const BALANCED_CONFIGURED_PRIMARY = "gpt-sol"; +const PREMIUM_CONFIGURED_PRIMARY = "gpt-astra"; const QUALITY_EXECUTION_FALLBACK = "openrouter/deepseek-v4-pro"; // An intentionally ambiguous prompt: no strong Layer-1 keyword signal. Paired diff --git a/apps/gateway/e2e/routing.spec.ts b/apps/gateway/e2e/routing.spec.ts index 1cd27b80..bd490140 100644 --- a/apps/gateway/e2e/routing.spec.ts +++ b/apps/gateway/e2e/routing.spec.ts @@ -23,9 +23,9 @@ const AUTH = { Authorization: `Bearer ${TEST_KEY}`, "Content-Type": "application // NO-SUBSCRIPTION FAIL-OPEN (the CI reality). Keep configured subscription // primaries distinct from the keyed static models that actually execute when no // OAuth account is bound. -const ECONOMY_CONFIGURED_PRIMARY = "gpt-6-luna"; -const PREMIUM_CONFIGURED_PRIMARY = "gpt-6-astra"; -const BALANCED_CONFIGURED_PRIMARY = "gpt-6.1-sol"; +const ECONOMY_CONFIGURED_PRIMARY = "gpt-luna"; +const PREMIUM_CONFIGURED_PRIMARY = "gpt-astra"; +const BALANCED_CONFIGURED_PRIMARY = "gpt-sol"; const ECONOMY_EXECUTION_FALLBACK = "openrouter/deepseek-v4-flash"; const QUALITY_EXECUTION_FALLBACK = "openrouter/deepseek-v4-pro"; // Paid OpenAI aliases are absent from shipped lanes but deliberately remain diff --git a/apps/gateway/src/routes/execute.default-config.test.ts b/apps/gateway/src/routes/execute.default-config.test.ts index 7b491d6b..0a9766c9 100644 --- a/apps/gateway/src/routes/execute.default-config.test.ts +++ b/apps/gateway/src/routes/execute.default-config.test.ts @@ -169,10 +169,10 @@ function providerClients(client: ProviderClient): Map { describe("default config activates capability filter + cost (alias-namespace alignment)", () => { it.each([ - "gpt-6-astra", - "gpt-6.1-sol", - "gpt-6.1-terra", - "gpt-6-luna", + "gpt-astra", + "gpt-sol", + "gpt-terra", + "gpt-luna", ])("the shipped %s model lane preserves the Codex client's reasoning effort", (lane) => { expect(lanes[lane]?.reasoning_effort).toBeUndefined(); }); @@ -182,16 +182,23 @@ describe("default config activates capability filter + cost (alias-namespace ali "gpt-6.1-terra", "gpt-6-luna", ])("routes shipped %s aliases and registers its official upstream", (model) => { - expect(lanes[model]?.primary).toBe(`openai-codex/${model}`); - expect(lanes[model]?.reasoning_effort).toBeUndefined(); - expect(expandChain(model)[1]).toBe("deepseek-responses/deepseek-flash"); + const laneNames: Record = { + "gpt-6.1-sol": "gpt-sol", + "gpt-6.1-terra": "gpt-terra", + "gpt-6-luna": "gpt-luna", + }; + const lane = laneNames[model]; + if (!lane) throw new Error(`Missing lane mapping for ${model}`); + expect(lanes[lane]?.primary).toBe(`openai-codex/${model}`); + expect(lanes[lane]?.reasoning_effort).toBeUndefined(); + expect(expandChain(lane)[1]).toBe("deepseek-responses/deepseek-flash"); for (const alias of [ model, `${model}-2026-09-25`, `openai.${model}`, `openai.${model}-2026-09-25`, ]) { - expect(resolveModelAlias(alias, config.model_aliases)).toBe(model); + expect(resolveModelAlias(alias, config.model_aliases)).toBe(lane); } expect( config.providers.find( diff --git a/config/lanes.yaml b/config/lanes.yaml index d5b30126..09eea210 100644 --- a/config/lanes.yaml +++ b/config/lanes.yaml @@ -23,26 +23,17 @@ # and lands on a deterministic json-capable model (docs/02: `*/auto` only at the # tail). -# —— quality/cost lanes —— -economy: - purpose: Cheap and fast for simple tasks - reasoning_effort: medium - primary: gpt-6-luna - fallback: - - claude-haiku - - grok - - openrouter/deepseek-v4-flash - - openrouter/auto - - zenmux/auto - constraints: - require_tools: false - require_json: false - require_vision: false +# Lane entries are sorted alphabetically. Upstream model IDs remain versioned. + + +bailian-speech: + primary: bailian/qwen-audio-3.1-tts-flash + fallback: [] balanced: purpose: Default quality/cost tradeoff (classification fallback terminal) reasoning_effort: medium - primary: gpt-6.1-sol + primary: gpt-sol fallback: - claude-sonnet - grok @@ -54,122 +45,34 @@ balanced: require_json: false require_vision: false -premium: - purpose: Strong reasoning and high quality - reasoning_effort: high - primary: gpt-6-astra - fallback: - - grok - - claude-opus - - balanced - constraints: - require_tools: false - require_json: false - require_vision: false - -# —— GPT-6 model lanes: Astra → Sol / Terra → Luna —— -# Subscription first, then DeepSeek Responses and the matching quality tier. -gpt-6-astra: - purpose: OpenAI GPT-6 Astra — flagship reasoning/coding - primary: openai-codex/gpt-6-astra +# Claude Fable 5.1 — the Mythos-class Claude (autonomous knowledge work + coding), +# 1M context. Mirrors claude-opus: LEADS with the native Anthropic OAuth alias (an +# unconnected `anthropic/*` candidate fails OPEN to the next fallback) and degrades +# straight into the generic `premium` chain. No static Fable mirror is wired, so +# without a Claude subscription this lane serves via `premium`. +claude-fable: + purpose: Anthropic Claude Fable 5.1 tier — Mythos-class autonomous work + primary: anthropic/claude-fable-5-1 fallback: - - deepseek-responses/deepseek-flash + - anthropic/claude-fable-5 - premium constraints: require_tools: false require_json: false require_vision: false -gpt-6.1-sol: - purpose: OpenAI GPT-6.1 Sol — complex coding and agentic workflows - primary: openai-codex/gpt-6.1-sol - fallback: - - deepseek-responses/deepseek-flash - - balanced - constraints: - require_tools: false - require_json: false - require_vision: false - -gpt-6.1-terra: - purpose: OpenAI GPT-6.1 Terra — visual reasoning, computer use, and writing - primary: openai-codex/gpt-6.1-terra - fallback: - - deepseek-responses/deepseek-flash - - balanced - constraints: - require_tools: false - require_json: false - require_vision: false - -gpt-6-luna: - purpose: OpenAI gpt-6-luna — efficient high-volume tasks - primary: openai-codex/gpt-6-luna +# Keep pinned Haiku traffic on the Anthropic subscription, then degrade directly +# to `economy` without automatically invoking the paid ZenMux Haiku alias. +claude-haiku: + purpose: Anthropic Claude Haiku tier — cheap/fast + primary: anthropic/claude-haiku-4-5-20251001 fallback: - - deepseek-responses/deepseek-flash - economy constraints: require_tools: false require_json: false require_vision: false -# —— task lanes (named after detected task_type; the lane resolver maps a -# classified task_type onto a same-named lane — docs/04) —— -coding: - purpose: Coding-capable models for code generation / editing - # GPT-6 Astra is the flagship coding/reasoning model. The legacy `*-codex` slugs - # are not used here because the ChatGPT-account backend historically rejected them. - reasoning_effort: high - primary: gpt-6-astra - fallback: - - premium - - balanced - constraints: - require_tools: false - require_json: false - require_vision: false - -json: - purpose: Strict structured-output (JSON) responses - # Sol supports JSON schema on Responses. The capability filter prunes - # incompatible fallbacks and the */auto tails for strict JSON requests. - primary: gpt-6.1-sol - fallback: - - claude-sonnet - - openrouter/deepseek-v4-flash - - balanced - constraints: - require_json: true - require_tools: false - require_vision: false - -vision: - purpose: Multimodal / image understanding - # Subscription-first: Sol handles everyday vision requests; Grok - # image input has also been verified live. Claude subscriptions cover structured - # and long-context vision without automatically invoking a paid ZenMux model. - primary: gpt-6.1-sol - fallback: - - grok - - claude-sonnet - - claude-opus - constraints: - require_vision: true - require_tools: false - require_json: false - -tool_use: - purpose: Reliable function / tool calling - reasoning_effort: medium - primary: gpt-6-astra - fallback: - - premium - constraints: - require_tools: true - require_json: false - require_vision: false - -# —— vendor-family lanes (targets of config/model-aliases.yaml) —— # A client that pins a FIXED vendor model id (Claude Code's `claude-opus-4-8`, an # SDK locked to `gpt-5.4`) has its `model` rewritten onto one of THESE lanes BEFORE # routing (model-aliases.yaml glob -> lane). Each LEADS with the requested family's @@ -198,25 +101,47 @@ claude-opus: require_json: false require_vision: false -# Claude Fable 5.1 — the Mythos-class Claude (autonomous knowledge work + coding), -# 1M context. Mirrors claude-opus: LEADS with the native Anthropic OAuth alias (an -# unconnected `anthropic/*` candidate fails OPEN to the next fallback) and degrades -# straight into the generic `premium` chain. No static Fable mirror is wired, so -# without a Claude subscription this lane serves via `premium`. -claude-fable: - purpose: Anthropic Claude Fable 5.1 tier — Mythos-class autonomous work - primary: anthropic/claude-fable-5-1 +claude-sonnet: + purpose: Anthropic Claude Sonnet tier — balanced Claude + primary: anthropic/claude-sonnet-5-5 + fallback: + - balanced + constraints: + require_tools: false + require_json: false + require_vision: false + +coding: + purpose: Coding-capable models for code generation / editing + # GPT-6 Astra is the flagship coding/reasoning model. The legacy `*-codex` slugs + # are not used here because the ChatGPT-account backend historically rejected them. + reasoning_effort: high + primary: gpt-astra fallback: - - anthropic/claude-fable-5 - premium + - balanced + constraints: + require_tools: false + require_json: false + require_vision: false +economy: + purpose: Cheap and fast for simple tasks + reasoning_effort: medium + primary: gpt-luna + fallback: + - claude-haiku + - grok + - openrouter/deepseek-v4-flash + - openrouter/auto + - zenmux/auto constraints: require_tools: false require_json: false require_vision: false -claude-sonnet: - purpose: Anthropic Claude Sonnet tier — balanced Claude - primary: anthropic/claude-sonnet-5-5 +gemini-flash: + purpose: Google Gemini Flash tier — fast multimodal + primary: zenmux-vertex/gemini-3.5-flash fallback: - balanced constraints: @@ -224,13 +149,12 @@ claude-sonnet: require_json: false require_vision: false -# Keep pinned Haiku traffic on the Anthropic subscription, then degrade directly -# to `economy` without automatically invoking the paid ZenMux Haiku alias. -claude-haiku: - purpose: Anthropic Claude Haiku tier — cheap/fast - primary: anthropic/claude-haiku-4-5-20251001 +gemini-image: + purpose: Gemini image — Google official → ZenMux Vertex flash → ZenMux pro + primary: google/gemini-3.1-flash-image fallback: - - economy + - gemini-3.1-flash-image + - gemini-3-pro-image constraints: require_tools: false require_json: false @@ -305,36 +229,18 @@ gemini-pro: require_json: false require_vision: false -gemini-flash: - purpose: Google Gemini Flash tier — fast multimodal - primary: zenmux-vertex/gemini-3.5-flash +# Subscription first, then DeepSeek Responses and the matching quality tier. +gpt-astra: + purpose: OpenAI GPT-6 Astra — flagship reasoning/coding + primary: openai-codex/gpt-6-astra fallback: - - balanced - constraints: - require_tools: false - require_json: false - require_vision: false - -grok: - purpose: Stable Grok channel; update only this primary when Grok changes version - primary: xai/grok-4.6 - fallback: [] - constraints: - require_tools: false - require_json: false - require_vision: false - -grok-fast: - purpose: Grok Build Fast channel — higher token prices for faster responses - primary: xai/grok-4.7-build-fast - fallback: [] + - deepseek-responses/deepseek-flash + - premium constraints: require_tools: false require_json: false require_vision: false - -# —— image-generation lanes (failover across image providers) —— # Image gen (POST /v1/images/generations, POST /v1beta/interactions, and Gemini # :generateContent on image models) is model-pinned per provider; group those # per-provider aliases into a LANE and the dedicated image endpoints fail over across @@ -356,12 +262,52 @@ gpt-image: require_json: false require_vision: false -gemini-image: - purpose: Gemini image — Google official → ZenMux Vertex flash → ZenMux pro - primary: google/gemini-3.1-flash-image +gpt-luna: + purpose: OpenAI gpt-6-luna — efficient high-volume tasks + primary: openai-codex/gpt-6-luna fallback: - - gemini-3.1-flash-image - - gemini-3-pro-image + - deepseek-responses/deepseek-flash + - economy + constraints: + require_tools: false + require_json: false + require_vision: false + +gpt-sol: + purpose: OpenAI GPT-6.1 Sol — complex coding and agentic workflows + primary: openai-codex/gpt-6.1-sol + fallback: + - deepseek-responses/deepseek-flash + - balanced + constraints: + require_tools: false + require_json: false + require_vision: false + +gpt-terra: + purpose: OpenAI GPT-6.1 Terra — visual reasoning, computer use, and writing + primary: openai-codex/gpt-6.1-terra + fallback: + - deepseek-responses/deepseek-flash + - balanced + constraints: + require_tools: false + require_json: false + require_vision: false + +grok: + purpose: Stable Grok channel; update only this primary when Grok changes version + primary: xai/grok-4.6 + fallback: [] + constraints: + require_tools: false + require_json: false + require_vision: false + +grok-fast: + purpose: Grok Build Fast channel — higher token prices for faster responses + primary: xai/grok-4.7-build-fast + fallback: [] constraints: require_tools: false require_json: false @@ -379,6 +325,15 @@ grok-imagine-image-quality: require_json: false require_vision: false +grok-imagine-video: + purpose: Grok Imagine multi-reference video generation via SuperGrok OAuth + primary: xai/grok-imagine-video + fallback: [] + constraints: + require_tools: false + require_json: false + require_vision: false + grok-imagine-video-1.5-preview: purpose: Grok Imagine single-image video generation via SuperGrok OAuth primary: xai/grok-imagine-video-1.5-preview @@ -388,27 +343,50 @@ grok-imagine-video-1.5-preview: require_json: false require_vision: false -grok-imagine-video: - purpose: Grok Imagine multi-reference video generation via SuperGrok OAuth - primary: xai/grok-imagine-video - fallback: [] +json: + purpose: Strict structured-output (JSON) responses + # Sol supports JSON schema on Responses. The capability filter prunes + # incompatible fallbacks and the */auto tails for strict JSON requests. + primary: gpt-sol + fallback: + - claude-sonnet + - openrouter/deepseek-v4-flash + - balanced constraints: + require_json: true require_tools: false - require_json: false require_vision: false -seedream-flash: - purpose: Ark dedicated media generation - primary: ark/seedream-5.0-flash +minimax-speech: + primary: minimax/speech-2.8-hd fallback: [] + +minimax-video: + primary: minimax/MiniMax-H3 + fallback: [] + +premium: + purpose: Strong reasoning and high quality + reasoning_effort: high + primary: gpt-astra + fallback: + - grok + - claude-opus + - balanced constraints: require_tools: false require_json: false require_vision: false -seedream-pro: +# Native media lanes use a single paid create; never replay via fallback. + +qwen-image: + primary: bailian/qwen-image-3.0-pro + fallback: [] + +seed3d: purpose: Ark dedicated media generation - primary: ark/seedream-5.0-pro + primary: ark/seed3d-2.0 fallback: [] constraints: require_tools: false @@ -424,41 +402,58 @@ seedance-video: require_json: false require_vision: false -seed3d: +seedream-flash: purpose: Ark dedicated media generation - primary: ark/seed3d-2.0 + primary: ark/seedream-5.0-flash fallback: [] constraints: require_tools: false require_json: false require_vision: false -# Native media lanes use a single paid create; never replay via fallback. - -qwen-image: - primary: bailian/qwen-image-3.0-pro - fallback: [] - -wan-video: - primary: bailian/wan3.0-video - fallback: [] - -minimax-video: - primary: minimax/MiniMax-H3 +seedream-pro: + purpose: Ark dedicated media generation + primary: ark/seedream-5.0-pro fallback: [] + constraints: + require_tools: false + require_json: false + require_vision: false -minimax-speech: - primary: minimax/speech-2.8-hd - fallback: [] +tool_use: + purpose: Reliable function / tool calling + reasoning_effort: medium + primary: gpt-astra + fallback: + - premium + constraints: + require_tools: true + require_json: false + require_vision: false -bailian-speech: - primary: bailian/qwen-audio-3.1-tts-flash +transcription-file: + primary: bailian/qwen3-asr-flash-filetrans fallback: [] transcription-short: primary: bailian/qwen3-asr-flash fallback: [] -transcription-file: - primary: bailian/qwen3-asr-flash-filetrans +vision: + purpose: Multimodal / image understanding + # Subscription-first: Sol handles everyday vision requests; Grok + # image input has also been verified live. Claude subscriptions cover structured + # and long-context vision without automatically invoking a paid ZenMux model. + primary: gpt-sol + fallback: + - grok + - claude-sonnet + - claude-opus + constraints: + require_vision: true + require_tools: false + require_json: false + +wan-video: + primary: bailian/wan3.0-video fallback: [] diff --git a/config/model-aliases.yaml b/config/model-aliases.yaml index 902caeaa..9d59a9ed 100644 --- a/config/model-aliases.yaml +++ b/config/model-aliases.yaml @@ -46,46 +46,46 @@ # (it is more literal than `gpt-5*`, so longest-literal-wins routes it to the cheap # lane, NOT the expensive `premium` catch-all). Comment these out if you don't front # any GPT clients. -"gpt-6-astra": gpt-6-astra -"gpt-6-astra-*": gpt-6-astra -"openai.gpt-6-astra": gpt-6-astra -"openai.gpt-6-astra-*": gpt-6-astra -"gpt-6.1-terra": gpt-6.1-terra -"gpt-6.1-terra-*": gpt-6.1-terra -"openai.gpt-6.1-terra": gpt-6.1-terra -"openai.gpt-6.1-terra-*": gpt-6.1-terra -"gpt-6.1-sol": gpt-6.1-sol -"gpt-6.1-sol-*": gpt-6.1-sol -"openai.gpt-6.1-sol": gpt-6.1-sol -"openai.gpt-6.1-sol-*": gpt-6.1-sol +"gpt-6-astra": gpt-astra +"gpt-6-astra-*": gpt-astra +"openai.gpt-6-astra": gpt-astra +"openai.gpt-6-astra-*": gpt-astra +"gpt-6.1-terra": gpt-terra +"gpt-6.1-terra-*": gpt-terra +"openai.gpt-6.1-terra": gpt-terra +"openai.gpt-6.1-terra-*": gpt-terra +"gpt-6.1-sol": gpt-sol +"gpt-6.1-sol-*": gpt-sol +"openai.gpt-6.1-sol": gpt-sol +"openai.gpt-6.1-sol-*": gpt-sol # Legacy Sol clients follow the current Sol lane. -"gpt-6-sol": gpt-6.1-sol -"gpt-6-sol-*": gpt-6.1-sol -"openai.gpt-6-sol": gpt-6.1-sol -"openai.gpt-6-sol-*": gpt-6.1-sol -"gpt-6-luna": gpt-6-luna -"gpt-6-luna-*": gpt-6-luna -"openai.gpt-6-luna": gpt-6-luna -"openai.gpt-6-luna-*": gpt-6-luna -"gpt-5.6": gpt-6-astra -"gpt-5.6-sol": gpt-6-astra -"gpt-5.6-sol-*": gpt-6-astra -"gpt-5.6-terra": gpt-6.1-sol -"gpt-5.6-terra-*": gpt-6.1-sol -"gpt-5.6-luna": gpt-6-luna -"gpt-5.6-luna-*": gpt-6-luna -"gpt-5.6-*": gpt-6-astra -"openai.gpt-5.6": gpt-6-astra -"openai.gpt-5.6-sol": gpt-6-astra -"openai.gpt-5.6-sol-*": gpt-6-astra -"openai.gpt-5.6-terra": gpt-6.1-sol -"openai.gpt-5.6-terra-*": gpt-6.1-sol -"openai.gpt-5.6-luna": gpt-6-luna -"openai.gpt-5.6-luna-*": gpt-6-luna -"openai.gpt-5.6-*": gpt-6-astra +"gpt-6-sol": gpt-sol +"gpt-6-sol-*": gpt-sol +"openai.gpt-6-sol": gpt-sol +"openai.gpt-6-sol-*": gpt-sol +"gpt-6-luna": gpt-luna +"gpt-6-luna-*": gpt-luna +"openai.gpt-6-luna": gpt-luna +"openai.gpt-6-luna-*": gpt-luna +"gpt-5.6": gpt-astra +"gpt-5.6-sol": gpt-astra +"gpt-5.6-sol-*": gpt-astra +"gpt-5.6-terra": gpt-sol +"gpt-5.6-terra-*": gpt-sol +"gpt-5.6-luna": gpt-luna +"gpt-5.6-luna-*": gpt-luna +"gpt-5.6-*": gpt-astra +"openai.gpt-5.6": gpt-astra +"openai.gpt-5.6-sol": gpt-astra +"openai.gpt-5.6-sol-*": gpt-astra +"openai.gpt-5.6-terra": gpt-sol +"openai.gpt-5.6-terra-*": gpt-sol +"openai.gpt-5.6-luna": gpt-luna +"openai.gpt-5.6-luna-*": gpt-luna +"openai.gpt-5.6-*": gpt-astra # GPT-5.4/5.5 are retired: the existing gpt-5* catch-all routes them to premium. -"gpt-5.4-mini": gpt-6-luna -"gpt-5.4-mini-*": gpt-6-luna +"gpt-5.4-mini": gpt-luna +"gpt-5.4-mini-*": gpt-luna "gpt-5-codex": coding "gpt-5*": premium "gpt-*": balanced @@ -123,3 +123,7 @@ "grok-imagine-video*": grok-imagine-video "grok*": grok grok-*-build-fast: grok-fast +gpt-astra: gpt-astra +gpt-luna: gpt-luna +gpt-sol: gpt-sol +gpt-terra: gpt-terra diff --git a/docs/04-routing-and-lanes.md b/docs/04-routing-and-lanes.md index d6177905..883ec1cf 100644 --- a/docs/04-routing-and-lanes.md +++ b/docs/04-routing-and-lanes.md @@ -290,19 +290,14 @@ quality/cost lanes by complexity (`simple → economy`, `medium → balanced`, ### Vendor-family lanes -Beyond the 7 generic lanes above, `config/lanes.yaml` ships **12 vendor-family -lanes** — the rewrite targets of the [model-alias compatibility -shim](#model-alias-compatibility-shim): - -```text -claude-opus claude-fable claude-sonnet claude-haiku -gpt-5.6 gpt-5.6-sol gpt-5.6-terra gpt-5.6-luna -gpt-5.4 gpt-5.4-mini -gemini-pro gemini-flash -``` - -(26 lanes total once `gpt-6-astra`, `grok` and the image/media lanes below are -included.) These +The version-independent GPT lanes are `gpt-astra`, `gpt-luna`, `gpt-sol`, and +`gpt-terra`. They currently target `gpt-6-astra`, `gpt-6-luna`, `gpt-6.1-sol`, +and `gpt-6.1-terra` upstream models respectively. Versioned client model IDs, +including dated and `openai.` variants, remain compatible through +`model-aliases.yaml`. Lane entries are listed alphabetically; execution order +still follows each lane's primary and fallback list. + +Vendor-family lanes exist so a client that pins a fixed vendor id lands on that family's real model instead of the GPT-led `premium` lane. Each one **leads with the requested vendor's native/subscription alias** — the Claude diff --git a/implementation-notes.md b/implementation-notes.md index c0477f35..6a24eca0 100644 --- a/implementation-notes.md +++ b/implementation-notes.md @@ -7,6 +7,12 @@ --- +## 2026-10-27 · GPT lane 去版本化并按名称排序(docs/04、06、11) + +- **命名**:`gpt-6-astra`、`gpt-6-luna`、`gpt-6.1-sol`、`gpt-6.1-terra` lane 分别改成 `gpt-astra`、`gpt-luna`、`gpt-sol`、`gpt-terra`;lane 配置按名称字母顺序排列。旧模型名、日期及 `openai.` 客户端别名继续映射到新 lane。 +- **边界**:只改 lane 名和引用,不改真实上游 model ID、价格、能力、reasoning effort 或 fallback 顺序。Admin、Portal 与模型发现继续读取配置;192.168.8.4 的 Helm 串联模型与目录键同步改名,避免把新 lane 名直接发送给真实模型上游。 +- **部署**:检查所有配置引用与现役 key 的 lane 作用域,保留无关自定义配置。v0.30.58 按发布工具同步稳定快照,分别读回两台服务器的排序与路由,并验证新名称及旧别名的真实流式响应。 + ## 2026-10-27 · 恢复 Sol 默认路由并保留独立 Terra lane(docs/04、06) - **用户修正**:保留 `gpt-6.1-sol` 与 `gpt-6.1-terra` 两个独立 lane;balanced、json、vision 及旧 Sol/GPT-5.6 Terra 客户端别名恢复指向 Sol。只有明确请求 `gpt-6.1-terra` 及其命名变体时进入 Terra lane,不把 Terra 作为其他 lane 的候选。 @@ -77,15 +83,10 @@ - **一致性**:共享发送边界覆盖原生/转换、HTTP/SSE、Codex WS/compact。每次克隆请求并清除失效 raw_body;实时读取手动设置,保持 Codex pool 和续接状态。历史环境只更新最近的已知块。 - **限制**:地理库误差、缓存期间 IP 轮换及按目的地分流无法由通用探测消除;上游已保存的历史不在新请求中时也无法改写。独立 Grok 遥测不经过网关。完整字段边界与调查版本见 `docs/subscription-timezones.md`。 -## 2026-10-14 · 模型目录超时与 Helm 串联握手(docs/05、06、07) - -- **根因**:版本模型目录的内部 5 秒期限被错误识别为客户端断开(499),早于 WebSocket 预检的 6 秒期限;已有 504 鉴权兜底未执行。中间 Helm 因握手失败改走 HTTP,随后带 `previous_response_id` 的续接缺少原 WebSocket。 -- **决定**:只在模型目录边界区分调用方取消和内部期限;真实取消优先,内部超时返回结构化 504,复用既有普通模型列表鉴权兜底。保留客户端权限、密钥撤销和有状态续接边界,不扩大重放范围。 -- **验证与限制**:定向回归覆盖超时分类、取消竞争、真实 socket 两轮复用和兜底时密钥撤销。目录本身较慢的上游原因不在本次修复内;未添加缓存、后台刷新或延长超时。 -- **发布前置**(docs/10):v0.30.47 使用 `release:prepare` 同步官方稳定快照;Claude 2.1.299 经完整性校验和独立静态审查。新增上传字段不进入推理白名单,原生 `cc_gateway` 归因保留但不仿造;Grok 1.0.52、Codex 0.162.1 与同 tag 模型目录保持最新。详情见 `docs/claude-protocol-safety.md`。 - ## 更早历史总览 +2026-10-14 模型目录超时与 Helm 串联握手的边界保持原有约定;完整记录见 git history。 + 2026-10-14:原币媒体价卡覆盖 25 个模型、57 个分档,保留原币和地域语义,未扩展结算或汇率换算;完整记录见 Git 历史及 docs/media-pricing.md。 2026-10-14:阿里与 MiniMax 媒体任务保留异步 operation ID、权限与下载边界;真实验收修正 Qwen3 OSS HTTPS 和固定 IP 连接,详见 Git 历史及 docs/media-provider-task-tracker.md。 diff --git a/package.json b/package.json index da2aab49..189e3c13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "helm-api", - "version": "0.30.57", + "version": "0.30.58", "private": true, "type": "module", "engines": { diff --git a/packages/core/src/config/rules-routing.test.ts b/packages/core/src/config/rules-routing.test.ts index e57b5705..04fb557f 100644 --- a/packages/core/src/config/rules-routing.test.ts +++ b/packages/core/src/config/rules-routing.test.ts @@ -101,7 +101,7 @@ describe("shipped config rules drive routing", () => { expect(config.lanes?.coding?.fallback).toEqual(["premium", "balanced"]); expect(config.lanes?.json?.constraints.require_json).toBe(true); expect(config.lanes?.vision).toMatchObject({ - primary: "gpt-6.1-sol", + primary: "gpt-sol", fallback: ["grok", "claude-sonnet", "claude-opus"], }); expect(config.lanes?.tool_use).toBeDefined(); @@ -141,7 +141,7 @@ describe("shipped config rules drive routing", () => { { allowCustomModel: true }, ); - expect(result.decision.lane.selected_lane).toBe("gpt-6-astra"); + expect(result.decision.lane.selected_lane).toBe("gpt-astra"); expect(result.decision.lane.candidate_chain[0]).toBe("openai-codex/gpt-6-astra"); expect(result.decision.lane.candidate_chain).not.toContain("openai/gpt-5.6"); expect(result.decision.requested_model).toBe("gpt-5.6"); diff --git a/packages/core/src/config/samples.test.ts b/packages/core/src/config/samples.test.ts index 7322b020..5c0cb423 100644 --- a/packages/core/src/config/samples.test.ts +++ b/packages/core/src/config/samples.test.ts @@ -42,19 +42,18 @@ describe("checked-in config samples", () => { const lanes = cfg.lanes ?? {}; const aliases = cfg.model_aliases ?? {}; const replacements = { - "gpt-6-sol": "gpt-6.1-sol", - "gpt-5.6": "gpt-6-astra", - "gpt-5.6-sol": "gpt-6-astra", - "gpt-5.6-terra": "gpt-6.1-sol", - "gpt-5.6-luna": "gpt-6-luna", - "gpt-5.4-mini": "gpt-6-luna", + "gpt-6-sol": "gpt-sol", + "gpt-5.6": "gpt-astra", + "gpt-5.6-sol": "gpt-astra", + "gpt-5.6-terra": "gpt-sol", + "gpt-5.6-luna": "gpt-luna", + "gpt-5.4-mini": "gpt-luna", }; - expect(Object.keys(lanes).filter((name) => /^gpt-6[.-]/.test(name))).toEqual([ - "gpt-6-astra", - "gpt-6.1-sol", - "gpt-6.1-terra", - "gpt-6-luna", - ]); + expect( + Object.keys(lanes).filter((name) => + ["gpt-astra", "gpt-luna", "gpt-sol", "gpt-terra"].includes(name), + ), + ).toEqual(["gpt-astra", "gpt-luna", "gpt-sol", "gpt-terra"]); for (const [old, current] of Object.entries(replacements)) { expect(lanes).not.toHaveProperty(old); expect(resolveModelAlias(old, aliases)).toBe(current); @@ -63,14 +62,39 @@ describe("checked-in config samples", () => { expect(resolveModelAlias(`openai.${old}`, aliases)).toBe(current); expect(resolveModelAlias(`openai.${old}-20260101`, aliases)).toBe(current); } - expect(expandLaneChain(current, lanes)[0]).toBe(`openai-codex/${current}`); + const upstreamModels: Record = { + "gpt-astra": "gpt-6-astra", + "gpt-luna": "gpt-6-luna", + "gpt-sol": "gpt-6.1-sol", + }; + const upstream = upstreamModels[current]; + expect(expandLaneChain(current, lanes)[0]).toBe(`openai-codex/${upstream}`); + } + expect(Object.keys(lanes)).toEqual(Object.keys(lanes).sort()); + for (const [model, lane] of Object.entries({ + "gpt-6-astra": "gpt-astra", + "gpt-6-luna": "gpt-luna", + "gpt-6.1-sol": "gpt-sol", + "gpt-6.1-terra": "gpt-terra", + })) { + expect(lanes).not.toHaveProperty(model); + for (const alias of [ + model, + `${model}-20261027`, + `openai.${model}`, + `openai.${model}-20261027`, + lane, + ]) { + expect(resolveModelAlias(alias, aliases)).toBe(lane); + } + expect(expandLaneChain(lane, lanes)[0]).toBe(`openai-codex/${model}`); } expect(lanes).not.toHaveProperty("gpt-5.4"); expect(resolveModelAlias("gpt-5.4", aliases)).toBe("premium"); for (const name of Object.keys(lanes)) { const chain = expandLaneChain(name, lanes); expect(chain.some((model) => /gpt-5\.(4|6)/.test(model))).toBe(false); - if (name !== "gpt-6.1-terra") { + if (name !== "gpt-terra") { expect(chain).not.toContain("openai-codex/gpt-6.1-terra"); } expect(new Set(chain).size).toBe(chain.length); @@ -135,7 +159,7 @@ describe("checked-in config samples", () => { expect(JSON.stringify(lanes)).not.toContain("zenmux-anthropic/claude-opus-4.8"); expect(JSON.stringify(lanes)).not.toContain("zenmux/gpt-5.5"); expect(JSON.stringify(lanes)).not.toContain("openai/gpt-5"); - expect(lanes.economy?.primary).toBe("gpt-6-luna"); + expect(lanes.economy?.primary).toBe("gpt-luna"); expect(lanes.economy?.fallback.slice(0, 5)).toEqual([ "claude-haiku", "grok", @@ -143,7 +167,7 @@ describe("checked-in config samples", () => { "openrouter/auto", "zenmux/auto", ]); - expect(lanes.balanced?.primary).toBe("gpt-6.1-sol"); + expect(lanes.balanced?.primary).toBe("gpt-sol"); expect(lanes.balanced?.fallback.slice(0, 5)).toEqual([ "claude-sonnet", "grok", @@ -152,7 +176,7 @@ describe("checked-in config samples", () => { "openrouter/auto", ]); expect(lanes.balanced?.fallback).not.toContain("anthropic/claude-sonnet-4-6"); - expect(lanes.premium?.primary).toBe("gpt-6-astra"); + expect(lanes.premium?.primary).toBe("gpt-astra"); expect(lanes.premium?.fallback).toEqual(["grok", "claude-opus", "balanced"]); expect(lanes.grok).toMatchObject({ primary: "xai/grok-4.6", fallback: [] }); expect(lanes["grok-fast"]).toMatchObject({ primary: "xai/grok-4.7-build-fast", fallback: [] }); @@ -161,14 +185,14 @@ describe("checked-in config samples", () => { expect(lanes.json?.constraints.require_json).toBe(true); // JSON stays subscription-first, with schema-capable fallbacks and a // capability filter that prunes incompatible candidates and auto tails. - expect(lanes.json?.primary).toBe("gpt-6.1-sol"); + expect(lanes.json?.primary).toBe("gpt-sol"); expect(lanes.json?.fallback).toEqual([ "claude-sonnet", "openrouter/deepseek-v4-flash", "balanced", ]); expect(lanes.vision?.constraints.require_vision).toBe(true); - expect(lanes.vision?.primary).toBe("gpt-6.1-sol"); + expect(lanes.vision?.primary).toBe("gpt-sol"); expect(lanes.vision?.fallback).toEqual(["grok", "claude-sonnet", "claude-opus"]); expect(lanes.tool_use?.constraints.require_tools).toBe(true); }); @@ -196,25 +220,25 @@ describe("checked-in config samples", () => { // GPT families route to their dedicated vendor-family lanes. The cheap mini must // NOT be swallowed by the broad `gpt-5*` -> premium catch-all (longest-literal // wins): a dated mini id still lands on the cheap GPT-6 Luna lane. - expect(resolveModelAlias("gpt-5.6", aliases)).toBe("gpt-6-astra"); - expect(resolveModelAlias("gpt-5.6-sol", aliases)).toBe("gpt-6-astra"); - expect(resolveModelAlias("gpt-5.6-sol-20260710", aliases)).toBe("gpt-6-astra"); - expect(resolveModelAlias("gpt-5.6-terra", aliases)).toBe("gpt-6.1-sol"); - expect(resolveModelAlias("gpt-5.6-terra-20260710", aliases)).toBe("gpt-6.1-sol"); - expect(resolveModelAlias("gpt-5.6-luna", aliases)).toBe("gpt-6-luna"); - expect(resolveModelAlias("gpt-5.6-luna-20260710", aliases)).toBe("gpt-6-luna"); - expect(resolveModelAlias("gpt-5.6-20260710", aliases)).toBe("gpt-6-astra"); - expect(resolveModelAlias("openai.gpt-5.6", aliases)).toBe("gpt-6-astra"); - expect(resolveModelAlias("openai.gpt-5.6-sol", aliases)).toBe("gpt-6-astra"); - expect(resolveModelAlias("openai.gpt-5.6-terra", aliases)).toBe("gpt-6.1-sol"); - expect(resolveModelAlias("openai.gpt-5.6-luna", aliases)).toBe("gpt-6-luna"); - expect(resolveModelAlias("openai.gpt-5.6-20260710", aliases)).toBe("gpt-6-astra"); + expect(resolveModelAlias("gpt-5.6", aliases)).toBe("gpt-astra"); + expect(resolveModelAlias("gpt-5.6-sol", aliases)).toBe("gpt-astra"); + expect(resolveModelAlias("gpt-5.6-sol-20260710", aliases)).toBe("gpt-astra"); + expect(resolveModelAlias("gpt-5.6-terra", aliases)).toBe("gpt-sol"); + expect(resolveModelAlias("gpt-5.6-terra-20260710", aliases)).toBe("gpt-sol"); + expect(resolveModelAlias("gpt-5.6-luna", aliases)).toBe("gpt-luna"); + expect(resolveModelAlias("gpt-5.6-luna-20260710", aliases)).toBe("gpt-luna"); + expect(resolveModelAlias("gpt-5.6-20260710", aliases)).toBe("gpt-astra"); + expect(resolveModelAlias("openai.gpt-5.6", aliases)).toBe("gpt-astra"); + expect(resolveModelAlias("openai.gpt-5.6-sol", aliases)).toBe("gpt-astra"); + expect(resolveModelAlias("openai.gpt-5.6-terra", aliases)).toBe("gpt-sol"); + expect(resolveModelAlias("openai.gpt-5.6-luna", aliases)).toBe("gpt-luna"); + expect(resolveModelAlias("openai.gpt-5.6-20260710", aliases)).toBe("gpt-astra"); expect(resolveModelAlias("gpt-5.4", aliases)).toBe("premium"); // RETIRED gpt-5.5 has no mapping of its own, so a client still pinning it falls // through `gpt-5*` onto premium — degraded, never a 400 unknown model. expect(resolveModelAlias("gpt-5.5", aliases)).toBe("premium"); - expect(resolveModelAlias("gpt-5.4-mini", aliases)).toBe("gpt-6-luna"); - expect(resolveModelAlias("gpt-5.4-mini-2026-01-01", aliases)).toBe("gpt-6-luna"); + expect(resolveModelAlias("gpt-5.4-mini", aliases)).toBe("gpt-luna"); + expect(resolveModelAlias("gpt-5.4-mini-2026-01-01", aliases)).toBe("gpt-luna"); // The shipped aliases must validate against the SHIPPED lanes (no drift): every // target is a configured lane or "auto", or the gateway would refuse to boot. expect(validateModelAliasTargets(aliases, laneNames)).toEqual([]); @@ -233,14 +257,14 @@ describe("checked-in config samples", () => { // and types the call out in its own notation. The fix is the provider's // `translateUnsupportedCustomTools` contract, not removing the rung. const deepseek = "deepseek-responses/deepseek-flash"; - expect(lanes["gpt-6.1-sol"]?.primary).toBe("openai-codex/gpt-6.1-sol"); - expect(lanes["gpt-6.1-sol"]?.fallback).toEqual([deepseek, "balanced"]); - expect(lanes["gpt-6.1-terra"]?.primary).toBe("openai-codex/gpt-6.1-terra"); - expect(lanes["gpt-6.1-terra"]?.fallback).toEqual([deepseek, "balanced"]); - expect(lanes["gpt-6-luna"]?.primary).toBe("openai-codex/gpt-6-luna"); - expect(lanes["gpt-6-luna"]?.fallback).toEqual([deepseek, "economy"]); - expect(lanes["gpt-6-astra"]?.primary).toBe("openai-codex/gpt-6-astra"); - expect(lanes["gpt-6-astra"]?.fallback).toEqual([deepseek, "premium"]); + expect(lanes["gpt-sol"]?.primary).toBe("openai-codex/gpt-6.1-sol"); + expect(lanes["gpt-sol"]?.fallback).toEqual([deepseek, "balanced"]); + expect(lanes["gpt-terra"]?.primary).toBe("openai-codex/gpt-6.1-terra"); + expect(lanes["gpt-terra"]?.fallback).toEqual([deepseek, "balanced"]); + expect(lanes["gpt-luna"]?.primary).toBe("openai-codex/gpt-6-luna"); + expect(lanes["gpt-luna"]?.fallback).toEqual([deepseek, "economy"]); + expect(lanes["gpt-astra"]?.primary).toBe("openai-codex/gpt-6-astra"); + expect(lanes["gpt-astra"]?.fallback).toEqual([deepseek, "premium"]); // Always `deepseek-flash`: `deepseek-v4-pro` has no image input, so a vision // request skips the rung entirely and the fallback silently does nothing. expect(JSON.stringify(lanes)).not.toContain("deepseek-responses/deepseek-v4-pro");