@bitkyc08/opencodex 2.62.0-preview.20260923 → 2.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/gui/dist/assets/App-BqrsSrIR.js +50 -0
  2. package/gui/dist/assets/{Tray-DdvPBVLi.js → Tray-CncKDBTp.js} +1 -1
  3. package/gui/dist/assets/index-C6SJrh0N.js +86 -0
  4. package/gui/dist/assets/index-DdDunwDb.css +1 -0
  5. package/gui/dist/assets/{usage-companion-chart-BIhvlbQW.js → usage-companion-chart-CzAAAB1o.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/cursor/catalog.ts +15 -0
  9. package/src/adapters/cursor/effort-map.ts +7 -0
  10. package/src/adapters/cursor/envelope-echo.ts +51 -25
  11. package/src/adapters/cursor/protobuf-request.ts +39 -13
  12. package/src/adapters/cursor.ts +52 -4
  13. package/src/adapters/devin/cloud-direct/chat.ts +50 -16
  14. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +4 -1
  15. package/src/adapters/devin/live-models.ts +7 -0
  16. package/src/adapters/devin.ts +7 -5
  17. package/src/adapters/kiro/reasoning.ts +5 -0
  18. package/src/adapters/openai-responses/passthrough.ts +4 -4
  19. package/src/adapters/openai-responses/tool-output-recovery.ts +5 -3
  20. package/src/claude/desktop-gateway-state.ts +7 -2
  21. package/src/claude/desktop-policy.ts +105 -1
  22. package/src/cli/config-command.ts +22 -7
  23. package/src/cli/doctor.ts +14 -4
  24. package/src/codex/catalog/effort.ts +35 -4
  25. package/src/codex/catalog/metadata.ts +33 -5
  26. package/src/codex/catalog/native-models.ts +43 -2
  27. package/src/codex/catalog/pinned-models.ts +37 -0
  28. package/src/codex/catalog-auto-refresh.ts +6 -0
  29. package/src/codex/data/roster-pinned-models.json +359 -0
  30. package/src/codex/data/upstream-models.json +365 -271
  31. package/src/codex/inject/provider-table.ts +108 -0
  32. package/src/codex/inject/remove.ts +2 -114
  33. package/src/codex/model-entitlements.ts +14 -10
  34. package/src/codex/subagent-defaults.ts +2 -109
  35. package/src/codex/toml-source-lines.ts +112 -0
  36. package/src/config/live-reconcile.ts +145 -27
  37. package/src/config/load-degrade.ts +3 -4
  38. package/src/config.ts +2 -2
  39. package/src/generated/compatibility-version.json +83 -67
  40. package/src/generated/model-metadata.ts +5 -5
  41. package/src/lab/artifacts/sanitize.ts +60 -19
  42. package/src/lib/app-owned-memory-stores.ts +7 -0
  43. package/src/lib/app-owned-memory.ts +13 -2
  44. package/src/lib/errors.ts +6 -4
  45. package/src/lib/upstream-retry.ts +35 -5
  46. package/src/oauth/devin.ts +43 -37
  47. package/src/providers/codebuddy-models.ts +11 -0
  48. package/src/providers/kiro-models.ts +9 -0
  49. package/src/providers/quota/vendor-probes-oauth.ts +9 -2
  50. package/src/providers/registry/entries-core.ts +19 -8
  51. package/src/providers/registry/entries-extended.ts +4 -1
  52. package/src/providers/registry/model-seeds.ts +22 -2
  53. package/src/responses/bridge-search-replay-cache.ts +20 -10
  54. package/src/responses/plaintext-v2-agent-messages.ts +10 -1
  55. package/src/routing/identity-domains.ts +22 -15
  56. package/src/server/index/websocket-handler.ts +22 -2
  57. package/src/server/management/agent-settings-routes.ts +19 -10
  58. package/src/server/management/context.ts +4 -2
  59. package/src/server/responses/codex-ws-exchange.ts +24 -8
  60. package/src/server/responses/core-codex-account.ts +4 -0
  61. package/src/server/responses/core-combo-failure.ts +16 -9
  62. package/src/server/responses/core-combo.ts +7 -3
  63. package/src/server/responses/core-options.ts +6 -0
  64. package/src/server/responses/native-injection-replay.ts +13 -1
  65. package/src/server/responses/native-injection.ts +66 -7
  66. package/src/server/responses/native-response-control.ts +6 -2
  67. package/src/server/responses/native-steering-replay.ts +60 -0
  68. package/src/server/responses/native-steering.ts +9 -1
  69. package/src/server/responses/passthrough-delivery.ts +3 -4
  70. package/src/server/responses/passthrough-dispatch.ts +7 -0
  71. package/src/server/responses/request-prepare.ts +12 -0
  72. package/src/server/responses/request-transport.ts +5 -2
  73. package/src/server/responses/ws-upstream.ts +1 -1
  74. package/src/server/ws-bridge.ts +17 -1
  75. package/src/types/request.ts +5 -0
  76. package/src/usage/expected-prices.ts +38 -8
  77. package/src/web-search/executor.ts +38 -13
  78. package/gui/dist/assets/App-CANWata-.js +0 -50
  79. package/gui/dist/assets/index-BIS5HXEP.js +0 -86
  80. package/gui/dist/assets/index-_bpvxJu0.css +0 -1
@@ -19,7 +19,7 @@ import { DEFAULT_REGION, type WindsurfRegion } from "./devin/types";
19
19
  import { registerUser } from "./devin/register-user";
20
20
  import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiBaseUrl, validateDevinApiBaseUrl } from "./devin/api-base";
21
21
  import { readDevinCliCredentialOutcome } from "./devin/cli-import";
22
- import { getCredential } from "./store";
22
+ import { getCredential, listAccounts } from "./store";
23
23
  import { DEPRECATED_OAUTH_PROVIDER_ALIASES } from "./index";
24
24
 
25
25
  export { DEVIN_DEFAULT_API_SERVER } from "./devin/api-base";
@@ -50,46 +50,52 @@ function devinAliasCredentialSlots(providerId: string): string[] {
50
50
  * configured provider baseUrl is the fallback, and the US default is the last
51
51
  * resort; both are re-validated because neither is trusted more than the
52
52
  * network value.
53
+ *
54
+ * A stored tenant host is used only for the account that owns `apiKey`, the key
55
+ * this request will transmit. Devin keeps several accounts per provider id and
56
+ * the request path injects the admitted account's token, which need not be the
57
+ * active one, while a provider-configured key, a forwarded bearer, or a test
58
+ * token is not stored at all. Reading the active slot's host for any of those
59
+ * would send one account's key to another account's EU or FedStart tenant.
53
60
  */
54
- export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "devin"): string {
55
- // Provider-scoped, keyed by the configured provider id verbatim and consulted
56
- // FIRST. `devin-cli` is a deprecated alias for `devin`, but an unmigrated
57
- // config row still owns its old credential slot until the startup migration
58
- // rekeys the row and the slot together — normalizing the id here would read
59
- // the wrong slot for that window. An EU or FedStart tenant is recorded on the
60
- // credential rather than in the registry, so a fixed "devin" slot would send
61
- // the key to the wrong host either way.
62
- const literalCredential = getCredential(providerId);
63
- const literal = validateDevinApiBaseUrl(literalCredential?.apiBaseUrl);
64
- if (literal !== undefined) return literal;
61
+ export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "devin", apiKey?: string): string {
62
+ const owner = apiKey ? findDevinCredentialOwner(providerId, apiKey) : undefined;
63
+ if (owner !== undefined) {
64
+ // The owning account decides. An owner whose recorded host is missing or
65
+ // off-allowlist falls through to the configured base URL rather than
66
+ // borrowing another slot's tenant: the same key stored twice is the rekey
67
+ // window, and a second slot's host is not more trustworthy than this one.
68
+ const host = validateDevinApiBaseUrl(owner.apiBaseUrl);
69
+ if (host !== undefined) return host;
70
+ }
71
+ return validateDevinApiBaseUrl(configuredBaseUrl) ?? DEVIN_DEFAULT_API_SERVER;
72
+ }
65
73
 
66
- // The startup merge saves providers["devin"] synchronously but fires the
67
- // credential rekey detached — runDevinProviderMergeStartupMigration cannot
68
- // await inside the synchronous startServer window — so the row can already
69
- // say "devin" while the credential still sits in the "devin-cli" slot, and it
70
- // stays that way for the whole process when the rekey fails or refuses on an
71
- // occupied destination slot. Reading the alias-linked slots in both
72
- // directions closes that window: "devin" finds the not-yet-rekeyed
73
- // "devin-cli" credential, and a lingering "devin-cli" row finds a credential
74
- // already rekeyed to "devin". Every candidate passes the same allowlist — an
75
- // alias slot is not trusted more than the literal one.
76
- // Only when this id owns no credential at all. A present credential whose
77
- // apiBaseUrl is missing or off-allowlist is a different situation: the rekey
78
- // refuses an occupied destination slot, so both ids can hold credentials that
79
- // belong to two different accounts. Borrowing a tenant across that pair would
80
- // send this account's key to the other account's EU or FedStart host, which
81
- // is the exact misdirection the provider-scoped lookup exists to prevent. An
82
- // unusable host on a credential that does exist falls through to the
83
- // configured base URL and then the default, as it did before this window was
84
- // closed.
85
- if (literalCredential === null || literalCredential === undefined) {
86
- for (const slot of devinAliasCredentialSlots(providerId)) {
87
- const host = validateDevinApiBaseUrl(getCredential(slot)?.apiBaseUrl);
88
- if (host !== undefined) return host;
74
+ /**
75
+ * The stored credential whose access token is exactly `apiKey`.
76
+ *
77
+ * The configured provider id is searched first and verbatim: `devin-cli` is a
78
+ * deprecated alias for `devin`, but an unmigrated config row still owns its old
79
+ * slot until the startup migration rekeys the row and the slot together. The
80
+ * startup merge saves providers["devin"] synchronously but fires the credential
81
+ * rekey detached (runDevinProviderMergeStartupMigration cannot await inside the
82
+ * synchronous startServer window), so the alias-linked slots are searched next,
83
+ * in both directions. Within a slot the active account is read first, then the
84
+ * rest, because the admitted account can be any of them.
85
+ *
86
+ * A key refreshed between token resolution and this lookup would match nothing
87
+ * and use the configured host for that turn. Devin keys are long-lived
88
+ * RegisterUser API keys, so this window is not a routine refresh race.
89
+ */
90
+ function findDevinCredentialOwner(providerId: string, apiKey: string): OAuthCredentials | undefined {
91
+ for (const slot of [providerId, ...devinAliasCredentialSlots(providerId)]) {
92
+ const active = getCredential(slot);
93
+ if (active?.access === apiKey) return active;
94
+ for (const account of listAccounts(slot)) {
95
+ if (account.credential.access === apiKey) return account.credential;
89
96
  }
90
97
  }
91
-
92
- return validateDevinApiBaseUrl(configuredBaseUrl) ?? DEVIN_DEFAULT_API_SERVER;
98
+ return undefined;
93
99
  }
94
100
 
95
101
  function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
@@ -21,6 +21,9 @@ export const CODEBUDDY_GLOBAL_MODELS = [
21
21
  "gpt-5.6-sol",
22
22
  "gpt-5.6-terra",
23
23
  "gpt-5.6-luna",
24
+ // 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows.
25
+ "gpt-6-sol",
26
+ "gpt-6-luna",
24
27
  "gpt-5.5",
25
28
  "gpt-5.4",
26
29
  "gpt-5.3-codex",
@@ -70,6 +73,8 @@ export const CODEBUDDY_GLOBAL_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
70
73
  "gpt-5.6-sol": 1_000_000,
71
74
  "gpt-5.6-terra": 1_000_000,
72
75
  "gpt-5.6-luna": 1_000_000,
76
+ "gpt-6-sol": 1_000_000,
77
+ "gpt-6-luna": 1_000_000,
73
78
  "gpt-5.5": 1_000_000,
74
79
  "gpt-5.4": 272_000,
75
80
  "gpt-5.3-codex": 272_000,
@@ -90,6 +95,8 @@ export const CODEBUDDY_GLOBAL_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> =
90
95
  "gpt-5.6-sol": 128_000,
91
96
  "gpt-5.6-terra": 128_000,
92
97
  "gpt-5.6-luna": 128_000,
98
+ "gpt-6-sol": 128_000,
99
+ "gpt-6-luna": 128_000,
93
100
  "gpt-5.5": 72_000,
94
101
  "gpt-5.4": 128_000,
95
102
  "gpt-5.3-codex": 128_000,
@@ -106,6 +113,8 @@ export const CODEBUDDY_GLOBAL_MODEL_REASONING_EFFORTS: Record<string, string[]>
106
113
  "gpt-5.6-sol": ["low", "medium", "high", "xhigh"],
107
114
  "gpt-5.6-terra": ["low", "medium", "high", "xhigh"],
108
115
  "gpt-5.6-luna": ["low", "medium", "high", "xhigh"],
116
+ "gpt-6-sol": ["low", "medium", "high", "xhigh"],
117
+ "gpt-6-luna": ["low", "medium", "high", "xhigh"],
109
118
  "glm-5.3": ["low", "high", "max"],
110
119
  "glm-5.2": ["high", "xhigh"],
111
120
  };
@@ -114,6 +123,8 @@ export const CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, st
114
123
  "gpt-5.6-sol": "high",
115
124
  "gpt-5.6-terra": "high",
116
125
  "gpt-5.6-luna": "high",
126
+ "gpt-6-sol": "high",
127
+ "gpt-6-luna": "high",
117
128
  "glm-5.3": "high",
118
129
  "glm-5.2": "high",
119
130
  };
@@ -4,7 +4,13 @@ export const KIRO_MODELS = [
4
4
  "gpt-5.6-sol",
5
5
  "gpt-5.6-terra",
6
6
  "gpt-5.6-luna",
7
+ // 260923 preemptive: GPT-6 Sol and Luna (OpenAI announced 2026-09-22) added ahead of this provider's own catalog; mirrors the GPT-5.6 Sol/Luna rows. Calls fail upstream until Kiro ships the models.
8
+ "gpt-6-sol",
9
+ "gpt-6-luna",
7
10
  "claude-sonnet-5",
11
+ // 260923 preemptive: Claude Opus 5.5 added ahead of Kiro's catalog (kiro.dev did not list it on
12
+ // 2026-09-23). Mirrors claude-opus-5; calls fail upstream until Kiro ships the model.
13
+ "claude-opus-5.5",
8
14
  "claude-opus-5",
9
15
  "claude-opus-4.8",
10
16
  "claude-opus-4.7",
@@ -28,7 +34,10 @@ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
28
34
  "gpt-5.6-sol": 272_000,
29
35
  "gpt-5.6-terra": 272_000,
30
36
  "gpt-5.6-luna": 272_000,
37
+ "gpt-6-sol": 272_000,
38
+ "gpt-6-luna": 272_000,
31
39
  "claude-sonnet-5": 1_000_000,
40
+ "claude-opus-5.5": 1_000_000,
32
41
  "claude-opus-5": 1_000_000,
33
42
  "claude-opus-4.8": 1_000_000,
34
43
  "claude-opus-4.7": 1_000_000,
@@ -224,6 +224,8 @@ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number
224
224
  return { percent, resetAt };
225
225
  }
226
226
 
227
+ const TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
228
+
227
229
  function parseClaudeLimit(value: unknown): { label: string; percent: number; resetAt?: number } | null {
228
230
  const rec = asRecord(value);
229
231
  if (!rec) return null;
@@ -231,13 +233,18 @@ function parseClaudeLimit(value: unknown): { label: string; percent: number; res
231
233
  if (percent === undefined) return null;
232
234
  const scope = asRecord(rec.scope);
233
235
  const model = asRecord(scope?.model);
234
- const rawLabel = String(model?.display_name ?? "").trim();
236
+ const rawLabel = String(model?.display_name ?? "")
237
+ .replace(TERMINAL_CONTROL_CHARACTERS, "")
238
+ .trim();
235
239
  if (!rawLabel) return null;
236
240
  const lowerLabel = rawLabel.toLowerCase();
237
241
  const label = lowerLabel.includes("fable") ? "Fable"
238
242
  : lowerLabel.includes("opus") ? "Opus"
239
243
  : lowerLabel.includes("sonnet") ? "Sonnet"
240
- : rawLabel;
244
+ : null;
245
+ // An unrecognized display_name is never published as a quota label: stripping
246
+ // control characters still leaves attacker-chosen residue on the quota line.
247
+ if (label === null) return null;
241
248
  const resetAt = normalizeResetAt(rec.resets_at);
242
249
  return { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) };
243
250
  }
@@ -23,6 +23,7 @@ import {
23
23
  ZAI_GLM_52_REASONING_EFFORTS,
24
24
  ZAI_GLM_53_REASONING_EFFORTS,
25
25
  OPENAI_GPT56_MODELS,
26
+ OPENAI_GPT6_MODELS,
26
27
  OPENAI_GPT56_PRO_MODELS,
27
28
  OPENAI_API_GPT56_CONTEXT_WINDOWS,
28
29
  OPENAI_API_GPT56_MAX_INPUT_TOKENS,
@@ -166,7 +167,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
166
167
  // (it is the current catalog, so its default ordering wins), then the ids
167
168
  // only the old devin entry carried. Degraded-mode seed only either way —
168
169
  // `liveModels` discovers the account's real roster.
169
- models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
170
+ models: ["swe-2", "swe-1-7", "gpt-5-6-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-sonnet-5", "glm-5-3", "kimi-k3", "gemini-3-8-flash", "grok-4-6", "swe-1-7-lightning", "gpt-5-6-luna", "gpt-5-6-terra", "claude-opus-4-8", "glm-5-2", "kimi-k2-7", "grok-4-5"],
170
171
  liveModels: true,
171
172
  defaultModel: "swe-2",
172
173
  modelContextWindows: DEVIN_MODEL_CONTEXT_WINDOWS,
@@ -535,13 +536,19 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
535
536
  featured: true,
536
537
  dashboardUrl: "https://platform.openai.com/api-keys",
537
538
  defaultModel: "gpt-5.5",
538
- models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS, "gpt-6-astra"],
539
+ models: ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS, "gpt-6-astra", ...OPENAI_GPT6_MODELS],
539
540
  liveModels: true,
540
- modelContextWindows: { ...OPENAI_API_GPT56_CONTEXT_WINDOWS, ...OPENAI_DAYBREAK_CONTEXT_WINDOWS, "gpt-6-astra": 1_050_000 },
541
- modelMaxInputTokens: { ...OPENAI_API_GPT56_MAX_INPUT_TOKENS, ...OPENAI_DAYBREAK_MAX_INPUT_TOKENS, "gpt-6-astra": 922_000 },
542
- modelMaxOutputTokens: { "gpt-6-astra": 128_000 },
541
+ modelContextWindows: {
542
+ ...OPENAI_API_GPT56_CONTEXT_WINDOWS, ...OPENAI_DAYBREAK_CONTEXT_WINDOWS, "gpt-6-astra": 1_050_000,
543
+ ...Object.fromEntries(OPENAI_GPT6_MODELS.map(id => [id, 1_050_000])),
544
+ },
545
+ modelMaxInputTokens: {
546
+ ...OPENAI_API_GPT56_MAX_INPUT_TOKENS, ...OPENAI_DAYBREAK_MAX_INPUT_TOKENS, "gpt-6-astra": 922_000,
547
+ ...Object.fromEntries(OPENAI_GPT6_MODELS.map(id => [id, 922_000])),
548
+ },
549
+ modelMaxOutputTokens: { "gpt-6-astra": 128_000, ...Object.fromEntries(OPENAI_GPT6_MODELS.map(id => [id, 128_000])) },
543
550
  modelInputModalities: Object.fromEntries(
544
- ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS, "gpt-6-astra"]
551
+ ["gpt-5.5", ...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS, ...OPENAI_DAYBREAK_MODELS, "gpt-6-astra", ...OPENAI_GPT6_MODELS]
545
552
  .map(id => [id, ["text", "image"]]),
546
553
  ),
547
554
  modelReasoningEfforts: {
@@ -550,6 +557,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
550
557
  ),
551
558
  ...OPENAI_DAYBREAK_REASONING_EFFORTS,
552
559
  "gpt-6-astra": ["low", "medium", "high", "xhigh", "max"],
560
+ ...Object.fromEntries(OPENAI_GPT6_MODELS.map(id => [id, ["low", "medium", "high", "xhigh", "max"]])),
553
561
  },
554
562
  virtualModels: OPENAI_API_GPT56_VIRTUAL_MODELS,
555
563
  },
@@ -870,7 +878,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
870
878
  featured: true,
871
879
  dashboardUrl: "https://openrouter.ai/keys",
872
880
  jawcodeBundle: "openrouter",
873
- models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS],
881
+ models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS, ...OPENAI_GPT6_MODELS.map(id => `openai/${id}`)],
874
882
  modelContextWindows: {
875
883
  "anthropic/claude-sonnet-5": 1_000_000,
876
884
  ...OPENROUTER_GPT56_CONTEXT_WINDOWS,
@@ -883,6 +891,9 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
883
891
  "openai/gpt-5.6-sol": true,
884
892
  "openai/gpt-5.6-terra": true,
885
893
  "openai/gpt-5.6-luna": true,
894
+ // 260923 preemptive: GPT-6 Sol/Luna are OpenAI-backed routes like the GPT-5.6 rows above.
895
+ "openai/gpt-6-sol": true,
896
+ "openai/gpt-6-luna": true,
886
897
  },
887
898
  // Deliberately no OpenRouter route pin: it bills the endpoint actually used and reports the
888
899
  // actual service_tier. B0 confirmation therefore owns downgrade safety. Forcing `only` plus
@@ -985,7 +996,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
985
996
  id: "bizrouter", label: "BizRouter", adapter: "openai-chat", baseUrl: "https://api.bizrouter.ai/v1",
986
997
  authKind: "key", dashboardUrl: "https://bizrouter.ai/settings/keys",
987
998
  defaultModel: "openai/gpt-5.6-sol",
988
- models: ["openai/gpt-5.6-sol", "anthropic/claude-sonnet-5", "google/gemini-3.5-flash"],
999
+ models: ["openai/gpt-5.6-sol", "openai/gpt-6-sol", "openai/gpt-6-luna", "anthropic/claude-sonnet-5", "google/gemini-3.5-flash"],
989
1000
  note: "Korean enterprise LLM gateway. Per-key allowed models are discovered live from /v1/models. Full catalog: https://bizrouter.ai/models",
990
1001
  },
991
1002
  { id: "groq", label: "Groq", adapter: "openai-chat", baseUrl: "https://api.groq.com/openai/v1", authKind: "key", featured: true, dashboardUrl: "https://console.groq.com/keys" },
@@ -1249,7 +1249,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1249
1249
  featured: false,
1250
1250
  dashboardUrl: "https://github.com/settings/copilot",
1251
1251
  liveModels: true,
1252
- models: ["gpt-4o", "gpt-4.1", "gpt-4.1-mini", "claude-sonnet-4", "gemini-2.5-pro", "gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
1252
+ models: ["gpt-4o", "gpt-4.1", "gpt-4.1-mini", "claude-sonnet-4", "gemini-2.5-pro", "gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-sol", "gpt-6-luna"],
1253
1253
  defaultModel: "gpt-4o",
1254
1254
  // Copilot fronts a mixed-wire catalog: these models reject /chat/completions for
1255
1255
  // real Codex-agent traffic (function tools + reasoning), so every inbound wire
@@ -1266,6 +1266,9 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1266
1266
  "gpt-5.6-sol": "openai-responses",
1267
1267
  "gpt-5.6-terra": "openai-responses",
1268
1268
  "gpt-6-astra": "openai-responses",
1269
+ // 260923 preemptive: GPT-6 Sol/Luna ride Responses like every GPT-5.6/6 row above.
1270
+ "gpt-6-sol": "openai-responses",
1271
+ "gpt-6-luna": "openai-responses",
1269
1272
  "grok-4.5": "openai-responses",
1270
1273
  "grok-4.6": "openai-responses",
1271
1274
  "mai-code-1.1-flash": "openai-responses",
@@ -6,8 +6,11 @@ import type { ProviderModelDiscoverySpec } from "./types";
6
6
  // devlog/_plan/260710_provider_hardening/001_research_frontier.md.
7
7
  // 260902 Claude Fable 5.1 (`claude-fable-5-1`): 1M context / 128K output / adaptive thinking
8
8
  // always on, per the official models overview and pricing page (platform.claude.com).
9
- export const ANTHROPIC_MODELS = ["claude-fable-5-1", "claude-fable-5", "claude-sonnet-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
10
- export const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-fable-5-1": 1_000_000, "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-4-5": 200_000 };
9
+ // 260923 Claude Opus 5.5 (`claude-opus-5-5`, released 2026-09-22): 1M context / 128K output /
10
+ // adaptive thinking always on / effort low..max with a medium default, per the Opus 5.5
11
+ // overview, effort and pricing pages (platform.claude.com).
12
+ export const ANTHROPIC_MODELS = ["claude-fable-5-1", "claude-fable-5", "claude-sonnet-5", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
13
+ export const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-fable-5-1": 1_000_000, "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-4-5": 200_000 };
11
14
  // All seeded Claude models support vision: https://platform.claude.com/docs/en/models/overview
12
15
  export const ANTHROPIC_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
13
16
  ANTHROPIC_MODELS.map(id => [id, ["text", "image"]]),
@@ -154,6 +157,12 @@ export const OPENAI_API_GPT56_VIRTUAL_MODELS: Record<string, { wireModelId: stri
154
157
  "gpt-5.6-luna-pro": { wireModelId: "gpt-5.6-luna", reasoningMode: "pro" },
155
158
  };
156
159
  export const OPENAI_API_GPT56_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
160
+ /**
161
+ * GPT-6 Sol and Luna on the OpenAI API (released 2026-09-22,
162
+ * https://developers.openai.com/api/docs/changelog). Added 2026-09-23 ahead of live discovery; the
163
+ * API window is not published yet, so the rows mirror gpt-6-astra's 1,050,000 / 922,000 API seed.
164
+ */
165
+ export const OPENAI_GPT6_MODELS = ["gpt-6-sol", "gpt-6-luna"];
157
166
  /*
158
167
  * Meta Model API (https://api.meta.ai/v1) — published ladder, deliberately NOT the
159
168
  * house set. dev.meta.ai/docs/reasoning lists "none", "minimal", "low", "medium",
@@ -238,6 +247,9 @@ export const OPENROUTER_GPT56_CONTEXT_WINDOWS = {
238
247
  "openai/gpt-5.6-sol": OPENROUTER_GPT56_CONTEXT_WINDOW,
239
248
  "openai/gpt-5.6-terra": OPENROUTER_GPT56_CONTEXT_WINDOW,
240
249
  "openai/gpt-5.6-luna": OPENROUTER_GPT56_CONTEXT_WINDOW,
250
+ // 260923 preemptive: GPT-6 Sol/Luna ahead of OpenRouter's own listing; same window as GPT-5.6.
251
+ "openai/gpt-6-sol": OPENROUTER_GPT56_CONTEXT_WINDOW,
252
+ "openai/gpt-6-luna": OPENROUTER_GPT56_CONTEXT_WINDOW,
241
253
  };
242
254
 
243
255
  /**
@@ -838,6 +850,9 @@ export const DIGITALOCEAN_CHAT_COMPLETION_MODELS = [
838
850
  "openai-gpt-5.6-sol",
839
851
  "openai-gpt-5.6-terra",
840
852
  "openai-gpt-5.6-luna",
853
+ // 260923 preemptive: GPT-6 Sol/Luna ahead of DigitalOcean's model list.
854
+ "openai-gpt-6-sol",
855
+ "openai-gpt-6-luna",
841
856
  "qwen3-coder-flash",
842
857
  "qwen3.5-397b-a17b",
843
858
  "deepseek-4-flash",
@@ -1003,8 +1018,11 @@ export const CLINE_PASS_MODEL_INPUT_MODALITIES: Record<string, string[]> = Objec
1003
1018
  // catalogue snapshot supplied by the original provider author
1004
1019
  // (https://api.opper.ai/v3/models?limit=2000, captured 2026-09-14); `vendor/model` ids
1005
1020
  // (anthropic/claude-sonnet-4-6) pin one route and stay valid, they are just not seeded.
1021
+ // 260923: `claude-opus-5-5` pool (anthropic, aws eu, vertex, vertex-eu members; all 1M / 128K,
1022
+ // vision) read from the same catalogue endpoint the day after Anthropic's release.
1006
1023
  export const OPPER_MODELS = [
1007
1024
  "claude-sonnet-4-6",
1025
+ "claude-opus-5-5",
1008
1026
  "claude-opus-5",
1009
1027
  "gpt-5.5",
1010
1028
  "gpt-5.4-mini",
@@ -1017,6 +1035,7 @@ export const OPPER_MODELS = [
1017
1035
  // (kimi-k3 output); live discovery owns which models exist.
1018
1036
  export const OPPER_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
1019
1037
  "claude-sonnet-4-6": 1_000_000,
1038
+ "claude-opus-5-5": 1_000_000,
1020
1039
  "claude-opus-5": 1_000_000,
1021
1040
  "gpt-5.5": 1_050_000,
1022
1041
  "gpt-5.4-mini": 400_000,
@@ -1027,6 +1046,7 @@ export const OPPER_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
1027
1046
  };
1028
1047
  export const OPPER_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
1029
1048
  "claude-sonnet-4-6": 64_000,
1049
+ "claude-opus-5-5": 128_000,
1030
1050
  "claude-opus-5": 128_000,
1031
1051
  "gpt-5.5": 128_000,
1032
1052
  "gpt-5.4-mini": 128_000,
@@ -14,9 +14,9 @@
14
14
  * what `appendBridgeSearchTurn` would have written onto a continuation leg, so a replayed turn
15
15
  * and a continued turn show the destination the same conversation.
16
16
  *
17
- * Scope. Entries are keyed by the upstream destination in addition to the cell id. The cell id is
18
- * a v4 UUID minted here, so it cannot collide across conversations, but an unscoped key would let
19
- * a history replayed against a DIFFERENT provider resurrect a call that provider never made.
17
+ * Scope. Entries are keyed by the exact conversation and serving identity in addition to the cell
18
+ * id. The cell id is a v4 UUID minted here, but possession of a client-visible id is not authority
19
+ * to recover result text under another provider, model, destination, or credential.
20
20
  *
21
21
  * Bounds and privacy. Result text is web content the caller already received, but it is still
22
22
  * request-derived data: it lives in memory only, is never logged, serialized, or exported, and is
@@ -26,7 +26,7 @@
26
26
  * alone. Neither re-running the search nor inventing a result is an acceptable recovery.
27
27
  */
28
28
 
29
- import { reasoningReplayDestinationIdentity } from "./reasoning-replay-cache";
29
+ import type { OcxReasoningReplayScopeRef } from "../types";
30
30
 
31
31
  const MAX_ENTRIES = 64;
32
32
  const MAX_TOTAL_BYTES = 512 * 1024;
@@ -58,14 +58,24 @@ let clockForTests: (() => number) | null = null;
58
58
  const now = (): number => clockForTests?.() ?? Date.now();
59
59
 
60
60
  /**
61
- * Identify the upstream destination a bridged search belongs to.
61
+ * Identify the exact conversation and upstream binding a bridged search belongs to.
62
62
  *
63
- * Reuses the salted process-local destination digest the reasoning replay cache already defines,
64
- * so both stores agree on what "the same upstream" means and neither invents a second notion of
65
- * destination identity.
63
+ * The serving route binds this holder only after provider, model, and physical credential
64
+ * selection. A missing conversation or binding fails closed: a cell id is client-visible and is
65
+ * not itself authority to recover another request's retained result.
66
66
  */
67
- export function bridgeSearchReplayScope(baseUrl: string | undefined): string | undefined {
68
- return reasoningReplayDestinationIdentity(baseUrl);
67
+ export function bridgeSearchReplayScope(scope: OcxReasoningReplayScopeRef | undefined): string | undefined {
68
+ const identity = scope?.current;
69
+ if (!scope?.clientPrincipalId || !scope.clientThreadId || !identity) return undefined;
70
+ return JSON.stringify([
71
+ scope.clientPrincipalId,
72
+ scope.clientThreadId,
73
+ identity.providerName,
74
+ identity.providerDestinationIdentity,
75
+ identity.adapterName,
76
+ identity.modelId,
77
+ identity.credentialIdentity,
78
+ ]);
69
79
  }
70
80
 
71
81
  function keyFor(scope: string, cellItemId: string): string {
@@ -94,7 +94,16 @@ function collaborationCatalogInfo(catalogs: readonly unknown[][]): {
94
94
 
95
95
  export function hasPlaintextV2CollaborationCatalog(body: unknown): boolean {
96
96
  if (!isPlainObject(body)) return false;
97
- return Array.isArray(body.tools) && collaborationCatalogInfo([body.tools]).hasV2Catalog;
97
+ if (Array.isArray(body.tools)) return collaborationCatalogInfo([body.tools]).hasV2Catalog;
98
+ // Responses Lite carries its default catalog as the first developer input item.
99
+ // An explicit top-level catalog wins; later historical catalogs are not defaults.
100
+ if (body.tools !== undefined || !Array.isArray(body.input)) return false;
101
+ const initial = body.input[0];
102
+ return isPlainObject(initial)
103
+ && initial.type === "additional_tools"
104
+ && initial.role === "developer"
105
+ && Array.isArray(initial.tools)
106
+ && collaborationCatalogInfo([initial.tools]).hasV2Catalog;
98
107
  }
99
108
 
100
109
  function hasOptimizedNamespaceConflict(catalogs: readonly unknown[][]): boolean {
@@ -259,38 +259,45 @@ function memberMatches(member: string, ref: CredentialDomainRef): boolean {
259
259
  }
260
260
 
261
261
  /**
262
- * Every way a declared grouping can be ambiguous, as operator-readable messages. The
263
- * config write path rejects on any of these and the load path drops the list, so an
264
- * ambiguous declaration is reported rather than resolved by whichever group came first.
262
+ * Every way a declared grouping can be ambiguous, reported by position only. Messages
263
+ * name group and member indexes, never the operator-supplied strings — a malformed
264
+ * credential pasted into this list would otherwise be printed verbatim into shared
265
+ * logs. The config write path rejects on any of these and the load path drops the
266
+ * list, so an ambiguous declaration is reported rather than resolved by whichever
267
+ * group came first.
265
268
  */
266
269
  export function credentialGroupIssues(groups: readonly DeclaredCredentialGroup[]): string[] {
267
270
  const issues: string[] = [];
268
- const seenIds = new Set<string>();
269
- const owner = new Map<string, string>();
270
- for (const group of groups) {
271
+ const seenIds = new Map<string, number>();
272
+ const owner = new Map<string, { groupIndex: number; memberIndex: number }>();
273
+ for (const [groupIndex, group] of groups.entries()) {
271
274
  // A duplicate id is not cosmetic: both groups key to `declared:<id>`, so the second
272
275
  // group's members join the first group's quota domain without anyone saying so.
273
- if (seenIds.has(group.id)) issues.push(`duplicate group id ${JSON.stringify(group.id)}`);
274
- seenIds.add(group.id);
276
+ const firstGroupIndex = seenIds.get(group.id);
277
+ if (firstGroupIndex !== undefined) {
278
+ issues.push(`duplicate group id at group index ${groupIndex} (first declared at group index ${firstGroupIndex})`);
279
+ } else {
280
+ seenIds.set(group.id, groupIndex);
281
+ }
275
282
  if (group.credentials.length === 0) {
276
- issues.push(`group ${JSON.stringify(group.id)} lists no credentials`);
283
+ issues.push(`group at index ${groupIndex} lists no credentials`);
277
284
  }
278
- for (const member of group.credentials) {
285
+ for (const [memberIndex, member] of group.credentials.entries()) {
279
286
  if (splitMember(member) === undefined) {
280
287
  issues.push(
281
- `group ${JSON.stringify(group.id)} member ${JSON.stringify(member)} must be provider-qualified as "<provider>:<credential-id>"`,
288
+ `credential at member index ${memberIndex} in group index ${groupIndex} must be provider-qualified as "<provider>:<credential-id>"`,
282
289
  );
283
290
  continue;
284
291
  }
285
292
  const existing = owner.get(canonicalMember(member));
286
- if (existing === group.id) {
287
- issues.push(`credential ${JSON.stringify(member)} is listed twice in group ${JSON.stringify(group.id)}`);
293
+ if (existing?.groupIndex === groupIndex) {
294
+ issues.push(`credential at member index ${memberIndex} is listed twice in group index ${groupIndex}`);
288
295
  } else if (existing !== undefined) {
289
296
  issues.push(
290
- `credential ${JSON.stringify(member)} is declared in more than one group (${existing}, ${group.id})`,
297
+ `credential at member index ${memberIndex} in group index ${groupIndex} is declared in more than one group (first declared at group index ${existing.groupIndex}, member index ${existing.memberIndex})`,
291
298
  );
292
299
  } else {
293
- owner.set(canonicalMember(member), group.id);
300
+ owner.set(canonicalMember(member), { groupIndex, memberIndex });
294
301
  }
295
302
  }
296
303
  }
@@ -86,6 +86,7 @@ import {
86
86
  } from "../live";
87
87
  import type { ServeOptionsContext } from "./serve-options";
88
88
  import type { RequestMetricsRecorder } from "../request-metrics";
89
+ import { resolveInboundBodyLimitBytes } from "../request-decompress";
89
90
 
90
91
  /**
91
92
  * The WebSocket half of the Bun.serve options, split out of serve-options.ts to keep that file
@@ -190,12 +191,31 @@ export function createWebsocketHandler(
190
191
  ws.close(1009, "message too large");
191
192
  return;
192
193
  }
194
+ // An established control connection only ever carries control frames, so the
195
+ // inbound body limit applies to raw bytes before the parse materializes them.
196
+ if (ws.data.nativeControl && rawBytes > resolveInboundBodyLimitBytes(config.maxInboundBodyBytes)) {
197
+ sendJsonFrame(ws, buildWsErrorFrame(413, {
198
+ type: "invalid_request_error",
199
+ code: "inbound_body_too_large",
200
+ message: "Native response control frame exceeds the configured inbound body limit.",
201
+ }));
202
+ return;
203
+ }
193
204
  let frame: Record<string, unknown>;
194
205
  try {
195
206
  frame = JSON.parse(typeof raw === "string" ? raw : raw.toString()) as Record<string, unknown>;
196
207
  } catch {
197
208
  return; // text-only contract; ignore unparseable frames
198
209
  }
210
+ if ((frame.type === "response.inject" || frame.type === "response.steer")
211
+ && rawBytes > resolveInboundBodyLimitBytes(config.maxInboundBodyBytes)) {
212
+ sendJsonFrame(ws, buildWsErrorFrame(413, {
213
+ type: "invalid_request_error",
214
+ code: "inbound_body_too_large",
215
+ message: "Native response control frame exceeds the configured inbound body limit.",
216
+ }));
217
+ return;
218
+ }
199
219
  if (frame.type === "response.inject" || frame.type === "response.steer" || (frame.type === "response.create" && ws.data.nativeControl)) {
200
220
  try {
201
221
  if (frame.type === "response.inject") {
@@ -227,8 +247,8 @@ export function createWebsocketHandler(
227
247
  const idleMs = typeof config.stallTimeoutSec === "number" && Number.isFinite(config.stallTimeoutSec)
228
248
  ? Math.max(1, config.stallTimeoutSec) * 1000 : 300_000;
229
249
  const mode = nativeResponseControlMode(frame, config);
230
- nativeControl = mode === "injection" ? new NativeInjectionChannel(frame, idleMs)
231
- : mode === "steering" ? new NativeSteeringChannel(frame, idleMs) : undefined;
250
+ nativeControl = mode === "injection" ? new NativeInjectionChannel(frame, idleMs, config.maxUpstreamBodyBytes)
251
+ : mode === "steering" ? new NativeSteeringChannel(frame, idleMs, config.maxUpstreamBodyBytes) : undefined;
232
252
  } catch {
233
253
  sendJsonFrame(ws, buildWsErrorFrame(400, { type: "invalid_request_error", message: "Invalid native steering request settings" }));
234
254
  return;