@bitkyc08/opencodex 2.56.0 → 2.57.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 (145) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-D4zuyIxQ.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +3 -3
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  8. package/src/adapters/command-code.ts +1 -1
  9. package/src/adapters/cursor/envelope-echo.ts +8 -2
  10. package/src/adapters/google.ts +7 -7
  11. package/src/adapters/kiro/payload.ts +17 -3
  12. package/src/adapters/kiro/reasoning.ts +70 -7
  13. package/src/adapters/kiro/stream.ts +8 -2
  14. package/src/adapters/kiro/wire.ts +2 -1
  15. package/src/adapters/kiro-events.ts +21 -13
  16. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  17. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  18. package/src/adapters/openai-chat.ts +8 -8
  19. package/src/adapters/openai-responses/passthrough.ts +32 -1
  20. package/src/bridge/errors.ts +26 -2
  21. package/src/bridge/response-json.ts +7 -1
  22. package/src/bridge/sse.ts +19 -1
  23. package/src/claude/desktop-profile.ts +66 -9
  24. package/src/claude/outbound.ts +18 -0
  25. package/src/cli/account-main.ts +1 -1
  26. package/src/cli/capabilities.ts +2 -2
  27. package/src/cli/combo.ts +10 -1
  28. package/src/cli/index.ts +48 -5
  29. package/src/cli/registry.ts +2 -1
  30. package/src/cli/system-command.ts +4 -4
  31. package/src/clients/config-export.ts +7 -3
  32. package/src/codex/account-label.ts +14 -3
  33. package/src/codex/account-store.ts +113 -26
  34. package/src/codex/account-usability.ts +21 -0
  35. package/src/codex/auth-api/login-flow.ts +14 -2
  36. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  37. package/src/codex/auth-context.ts +157 -7
  38. package/src/codex/catalog/aggregation.ts +80 -1
  39. package/src/codex/catalog/model-visibility.ts +1 -0
  40. package/src/codex/catalog/remote.ts +30 -0
  41. package/src/codex/catalog/retained-sync.ts +9 -1
  42. package/src/codex/catalog/routed-gather.ts +38 -1
  43. package/src/codex/cli-install-provenance.ts +7 -1
  44. package/src/codex/convergence.ts +7 -2
  45. package/src/codex/desktop-app/types.ts +11 -2
  46. package/src/codex/desktop-app/windows.ts +5 -5
  47. package/src/codex/inject/restore.ts +29 -2
  48. package/src/codex/inject.ts +9 -9
  49. package/src/codex/model-entitlements.ts +152 -15
  50. package/src/codex/pool-refresh-backoff.ts +12 -3
  51. package/src/codex/quota-rejection.ts +104 -15
  52. package/src/codex/routing/cache-affinity.ts +70 -0
  53. package/src/codex/routing/cooldown-math.ts +10 -0
  54. package/src/codex/routing/selection.ts +79 -2
  55. package/src/codex/routing/thread-affinity.ts +50 -2
  56. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  57. package/src/codex/routing.ts +29 -49
  58. package/src/codex/warmup.ts +1 -1
  59. package/src/combos/failover.ts +85 -0
  60. package/src/combos/request.ts +17 -10
  61. package/src/combos/types.ts +23 -2
  62. package/src/config/pending-teardown.ts +31 -0
  63. package/src/generated/compatibility-version.json +163 -135
  64. package/src/images/loop.ts +1 -1
  65. package/src/lib/errors.ts +17 -0
  66. package/src/lib/request-execution-budget.ts +147 -21
  67. package/src/lib/spend-reservation-ledger.ts +18 -0
  68. package/src/lib/state-store-registrations.ts +6 -2
  69. package/src/lib/test-home-guard.ts +85 -1
  70. package/src/lib/upstream-retry.ts +77 -10
  71. package/src/lib/windows-elevation.ts +76 -14
  72. package/src/oauth/index.ts +2 -2
  73. package/src/oauth/key-providers.ts +2 -2
  74. package/src/providers/kiro-models.ts +4 -3
  75. package/src/providers/label.ts +19 -1
  76. package/src/providers/model-discovery.ts +16 -0
  77. package/src/providers/registry/entries-core.ts +7 -0
  78. package/src/providers/registry/entries-extended.ts +9 -0
  79. package/src/providers/registry/model-seeds.ts +4 -0
  80. package/src/responses/reasoning-envelope.ts +6 -3
  81. package/src/routing/identity-domains.ts +21 -14
  82. package/src/routing/probe-lease.ts +103 -1
  83. package/src/server/chat-completions.ts +3 -1
  84. package/src/server/chat-native.ts +37 -9
  85. package/src/server/index/live-sideband.ts +37 -1
  86. package/src/server/index/websocket-handler.ts +6 -2
  87. package/src/server/index.ts +5 -5
  88. package/src/server/inspection-tee.ts +107 -0
  89. package/src/server/live.ts +46 -1
  90. package/src/server/management/combo-routes.ts +10 -1
  91. package/src/server/relay-eager.ts +2 -0
  92. package/src/server/relay.ts +14 -19
  93. package/src/server/request-log.ts +127 -3
  94. package/src/server/response-log-body.ts +153 -0
  95. package/src/server/responses/account-change-state.ts +74 -0
  96. package/src/server/responses/adapter-continuation.ts +33 -7
  97. package/src/server/responses/adapter-delivery.ts +5 -11
  98. package/src/server/responses/adapter-dispatch.ts +84 -13
  99. package/src/server/responses/codex-ws-wire.ts +5 -0
  100. package/src/server/responses/collaboration.ts +74 -4
  101. package/src/server/responses/combo-session-recall.ts +68 -8
  102. package/src/server/responses/compact.ts +54 -13
  103. package/src/server/responses/core-auth.ts +2 -0
  104. package/src/server/responses/core-codex-account.ts +51 -3
  105. package/src/server/responses/core-combo.ts +103 -23
  106. package/src/server/responses/core-errors.ts +18 -0
  107. package/src/server/responses/core-replay.ts +105 -32
  108. package/src/server/responses/core.ts +3 -3
  109. package/src/server/responses/encrypted-payload.ts +0 -1
  110. package/src/server/responses/input-admission.ts +126 -6
  111. package/src/server/responses/passthrough-delivery.ts +19 -6
  112. package/src/server/responses/passthrough-dispatch.ts +28 -10
  113. package/src/server/responses/passthrough-error.ts +38 -2
  114. package/src/server/responses/request-prepare.ts +132 -22
  115. package/src/server/responses/request-send-budget.ts +97 -2
  116. package/src/server/responses/request-spend.ts +147 -0
  117. package/src/server/responses/request-transport.ts +62 -3
  118. package/src/server/responses/run-turn-execution.ts +59 -31
  119. package/src/server/responses/sidecar-execution.ts +7 -13
  120. package/src/server/responses/terminal-guard.ts +65 -4
  121. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  122. package/src/service/windows-ops.ts +210 -16
  123. package/src/service/windows-scheduler.ts +28 -21
  124. package/src/service.ts +1 -1
  125. package/src/types/config.ts +4 -1
  126. package/src/types/request.ts +8 -5
  127. package/src/types/tools.ts +24 -0
  128. package/src/types.ts +2 -0
  129. package/src/update/index.ts +10 -0
  130. package/src/update/stop-contract.d.mts +1 -0
  131. package/src/update/stop-contract.mjs +19 -0
  132. package/src/update/stop-decision.d.mts +1 -1
  133. package/src/update/stop-decision.mjs +12 -3
  134. package/src/usage/log.ts +1 -1
  135. package/src/vision/anthropic-describe.ts +1 -1
  136. package/src/vision/describe.ts +5 -5
  137. package/src/web-search/anthropic-executor.ts +1 -1
  138. package/src/web-search/exa-executor.ts +1 -1
  139. package/src/web-search/executor.ts +1 -1
  140. package/src/web-search/gemini-executor.ts +1 -1
  141. package/src/web-search/loop.ts +1 -1
  142. package/src/web-search/ollama-executor.ts +1 -1
  143. package/src/web-search/parse.ts +67 -14
  144. package/src/web-search/passthrough-bridge.ts +64 -31
  145. package/src/web-search/xai-executor.ts +1 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-D4zuyIxQ.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-BBOZWGB6.css">
19
+ <script type="module" crossorigin src="/assets/index-Cz7CLdif.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-C5-RdDmD.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.56.0",
3
+ "version": "2.57.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -76,11 +76,11 @@
76
76
  "@bufbuild/protobuf": "^2.14.0",
77
77
  "@modelcontextprotocol/sdk": "^1.30.0",
78
78
  "@napi-rs/keyring": "1.3.0",
79
- "bun": "1.4.2",
79
+ "bun": "1.4.0",
80
80
  "zod": "4.4.3"
81
81
  },
82
82
  "devDependencies": {
83
- "@types/bun": "1.4.2",
83
+ "@types/bun": "1.4.0",
84
84
  "typescript": "7.0.2"
85
85
  },
86
86
  "overrides": {
@@ -4,6 +4,7 @@ import { mapReasoningEffort } from "../../reasoning-effort";
4
4
  import { buildSystemPrompt } from "../coding-agent/protocol";
5
5
  import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type SpawnFn } from "../coding-agent/turn";
6
6
  import { CODEBUDDY_PROFILES, type CodeBuddyProfile } from "./profiles";
7
+ import { guardCodeBuddyScaffolding } from "./scaffold-guard";
7
8
 
8
9
  export type { SpawnFn } from "../coding-agent/turn";
9
10
  export type CodeBuddyAdapterDeps = CodingAgentDeps;
@@ -75,7 +76,7 @@ export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBu
75
76
  provider,
76
77
  parsed,
77
78
  incoming,
78
- emit,
79
+ emit: guardCodeBuddyScaffolding(emit),
79
80
  buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov),
80
81
  buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey),
81
82
  deps,
@@ -0,0 +1,248 @@
1
+ import type { AdapterEvent } from "../../types";
2
+
3
+ /** Error code for a CodeBuddy turn whose output contains vendor agent scaffolding. */
4
+ export const CODEBUDDY_SCAFFOLD_ERROR_CODE = "vendor_scaffold_detected";
5
+
6
+ // The observed control protocol uses FULLWIDTH VERTICAL LINE (U+FF5C). Detection stays
7
+ // deliberately narrower than the marker spelling: a calls control line must be followed by an
8
+ // invoke line for a functions.* tool. That distinguishes an agent scaffold from prose quoting or
9
+ // discussing one tag.
10
+ const DSML_CALLS_LINE = "<||dsml|| calls>";
11
+ const DSML_INVOKE_PREFIX = "<||dsml|| invoke name=\"functions.";
12
+
13
+ export interface CodeBuddyScaffoldFilterResult {
14
+ /** Bytes released from a suffix withheld by an earlier event on this channel. */
15
+ releasedPending: string;
16
+ /** Safe bytes belonging to the event currently being processed. */
17
+ text: string;
18
+ /** The earlier pending event still owns the extended candidate. */
19
+ pendingContinues: boolean;
20
+ fail: boolean;
21
+ }
22
+
23
+ interface ScanResult {
24
+ safe: string;
25
+ held: string;
26
+ fail: boolean;
27
+ fence: "`" | "~" | null;
28
+ lineStart: boolean;
29
+ }
30
+
31
+ function prefixAtEnd(text: string, at: number, expected: string): boolean {
32
+ const rest = text.slice(at).toLowerCase();
33
+ return rest.length < expected.length && expected.startsWith(rest);
34
+ }
35
+
36
+ /**
37
+ * Scan complete bytes and retain only a bounded suffix that can still become a control sequence.
38
+ *
39
+ * Control tags are recognized only at column zero and outside fenced Markdown. Inline code,
40
+ * quoted strings, blockquotes, indented source, and prose all add syntax before the tag and are
41
+ * therefore forwarded unchanged. A calls line alone is harmless; refusal requires the observed
42
+ * two-line calls-plus-functions-invoke grammar.
43
+ */
44
+ function scan(
45
+ text: string,
46
+ initialFence: "`" | "~" | null,
47
+ initialLineStart: boolean,
48
+ ): ScanResult {
49
+ let fence = initialFence;
50
+ let lineStart = initialLineStart;
51
+ let index = 0;
52
+
53
+ while (index < text.length) {
54
+ if (lineStart) {
55
+ const fenceMarkers = fence ? [fence.repeat(3)] : ["```", "~~~"];
56
+ const completeFence = fenceMarkers.find(marker => text.startsWith(marker, index));
57
+ if (completeFence) {
58
+ fence = fence ? null : (completeFence[0] as "`" | "~");
59
+ index += completeFence.length;
60
+ lineStart = false;
61
+ continue;
62
+ }
63
+ if (fenceMarkers.some(marker => prefixAtEnd(text, index, marker))) {
64
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
65
+ }
66
+
67
+ if (!fence) {
68
+ const lowered = text.slice(index).toLowerCase();
69
+ if (lowered.startsWith(DSML_CALLS_LINE)) {
70
+ const afterCalls = index + DSML_CALLS_LINE.length;
71
+ let invokeAt = -1;
72
+ if (text[afterCalls] === "\n") invokeAt = afterCalls + 1;
73
+ else if (text[afterCalls] === "\r" && text[afterCalls + 1] === "\n") invokeAt = afterCalls + 2;
74
+ else if (afterCalls === text.length || (text[afterCalls] === "\r" && afterCalls + 1 === text.length)) {
75
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
76
+ }
77
+
78
+ if (invokeAt >= 0) {
79
+ const invokeRest = text.slice(invokeAt).toLowerCase();
80
+ if (invokeRest.startsWith(DSML_INVOKE_PREFIX)) {
81
+ return { safe: text.slice(0, index), held: "", fail: true, fence, lineStart };
82
+ }
83
+ if (invokeRest.length === 0 || DSML_INVOKE_PREFIX.startsWith(invokeRest)) {
84
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
85
+ }
86
+ }
87
+ } else if (prefixAtEnd(text, index, DSML_CALLS_LINE)) {
88
+ return { safe: text.slice(0, index), held: text.slice(index), fail: false, fence, lineStart };
89
+ }
90
+ }
91
+ }
92
+
93
+ const char = text[index]!;
94
+ index += 1;
95
+ lineStart = char === "\n";
96
+ }
97
+
98
+ return { safe: text, held: "", fail: false, fence, lineStart };
99
+ }
100
+
101
+ /** Streaming DSML control-sequence filter for one text or reasoning channel. */
102
+ export class CodeBuddyScaffoldFilter {
103
+ private pending = "";
104
+ private failed = false;
105
+ private fence: "`" | "~" | null = null;
106
+ private lineStart = true;
107
+
108
+ /** True while an earlier event owns an unresolved marker or fence prefix. */
109
+ hasPending(): boolean {
110
+ return this.pending.length > 0;
111
+ }
112
+
113
+ push(chunk: string): CodeBuddyScaffoldFilterResult {
114
+ if (this.failed) {
115
+ return { releasedPending: "", text: "", pendingContinues: false, fail: false };
116
+ }
117
+ if (!chunk) {
118
+ return {
119
+ releasedPending: "",
120
+ text: "",
121
+ pendingContinues: this.hasPending(),
122
+ fail: false,
123
+ };
124
+ }
125
+
126
+ const priorPending = this.pending;
127
+ const result = scan(priorPending + chunk, this.fence, this.lineStart);
128
+ this.pending = result.held;
129
+ this.fence = result.fence;
130
+ this.lineStart = result.lineStart;
131
+ this.failed = result.fail;
132
+
133
+ const releasedLength = Math.min(priorPending.length, result.safe.length);
134
+ return {
135
+ releasedPending: result.safe.slice(0, releasedLength),
136
+ text: result.safe.slice(releasedLength),
137
+ pendingContinues: priorPending.length > 0 && result.safe.length === 0 && result.held.length > 0,
138
+ fail: result.fail,
139
+ };
140
+ }
141
+
142
+ /** Release a suffix that never completed the two-line control grammar. */
143
+ flush(): CodeBuddyScaffoldFilterResult {
144
+ if (this.failed) {
145
+ return { releasedPending: "", text: "", pendingContinues: false, fail: false };
146
+ }
147
+ const text = this.pending;
148
+ this.pending = "";
149
+ return { releasedPending: text, text: "", pendingContinues: false, fail: false };
150
+ }
151
+ }
152
+
153
+ function codeBuddyScaffoldErrorMessage(): string {
154
+ return "CodeBuddy CLI emitted vendor tool-call markup in an assistant output channel. This route"
155
+ + " runs the CLI with its own tools and MCP servers disabled and Codex owns tool control, so"
156
+ + " the turn was refused rather than forwarding or executing vendor agent scaffolding.";
157
+ }
158
+
159
+ /** Guard both streamed channels while preserving event order around withheld marker prefixes. */
160
+ export function guardCodeBuddyScaffolding(emit: (event: AdapterEvent) => void): (event: AdapterEvent) => void {
161
+ const textFilter = new CodeBuddyScaffoldFilter();
162
+ const thinkingFilter = new CodeBuddyScaffoldFilter();
163
+ type PendingChannel = "text" | "thinking";
164
+ type EventSlot = { resolved: boolean; event?: AdapterEvent };
165
+ const eventQueue: EventSlot[] = [];
166
+ const pendingSlots = new Map<PendingChannel, EventSlot>();
167
+ let closed = false;
168
+
169
+ const channelEvent = (channel: PendingChannel, text: string): AdapterEvent => channel === "text"
170
+ ? { type: "text_delta", text }
171
+ : { type: "thinking_delta", thinking: text };
172
+
173
+ const drainResolved = (): void => {
174
+ while (eventQueue[0]?.resolved) {
175
+ const slot = eventQueue.shift()!;
176
+ if (slot.event) emit(slot.event);
177
+ }
178
+ };
179
+
180
+ const enqueueResolved = (event: AdapterEvent): void => {
181
+ eventQueue.push({ resolved: true, event });
182
+ drainResolved();
183
+ };
184
+
185
+ const resolvePendingSlot = (channel: PendingChannel, text: string): void => {
186
+ const slot = pendingSlots.get(channel);
187
+ if (!slot) return;
188
+ slot.resolved = true;
189
+ if (text) slot.event = channelEvent(channel, text);
190
+ pendingSlots.delete(channel);
191
+ drainResolved();
192
+ };
193
+
194
+ const enqueuePendingSlot = (channel: PendingChannel): void => {
195
+ const slot: EventSlot = { resolved: false };
196
+ eventQueue.push(slot);
197
+ pendingSlots.set(channel, slot);
198
+ };
199
+
200
+ const flushAllPending = (): void => {
201
+ for (const channel of ["text", "thinking"] as const) {
202
+ if (!pendingSlots.has(channel)) continue;
203
+ const filter = channel === "text" ? textFilter : thinkingFilter;
204
+ resolvePendingSlot(channel, filter.flush().releasedPending);
205
+ }
206
+ drainResolved();
207
+ };
208
+
209
+ const refuse = (): void => {
210
+ if (closed) return;
211
+ flushAllPending();
212
+ closed = true;
213
+ emit({
214
+ type: "error",
215
+ message: codeBuddyScaffoldErrorMessage(),
216
+ status: 502,
217
+ errorType: "upstream_error",
218
+ code: CODEBUDDY_SCAFFOLD_ERROR_CODE,
219
+ retryable: false,
220
+ });
221
+ };
222
+
223
+ return (event: AdapterEvent): void => {
224
+ if (closed) return;
225
+ if (event.type === "text_delta" || event.type === "thinking_delta") {
226
+ const channel: PendingChannel = event.type === "text_delta" ? "text" : "thinking";
227
+ const filter = channel === "text" ? textFilter : thinkingFilter;
228
+ const hadPending = filter.hasPending();
229
+ const cleaned = filter.push(event.type === "text_delta" ? event.text : event.thinking);
230
+ if (hadPending && !cleaned.pendingContinues) resolvePendingSlot(channel, cleaned.releasedPending);
231
+ if (cleaned.text) {
232
+ enqueueResolved(event.type === "text_delta"
233
+ ? { ...event, text: cleaned.text }
234
+ : { ...event, thinking: cleaned.text });
235
+ }
236
+ if (filter.hasPending() && !cleaned.pendingContinues) enqueuePendingSlot(channel);
237
+ if (cleaned.fail) refuse();
238
+ return;
239
+ }
240
+ if (event.type === "done" || event.type === "error" || event.type === "incomplete") {
241
+ flushAllPending();
242
+ closed = true;
243
+ emit(event);
244
+ return;
245
+ }
246
+ enqueueResolved(event);
247
+ };
248
+ }
@@ -469,7 +469,7 @@ async function fetchCommandCode(request: AdapterRequest, ctx: AdapterFetchContex
469
469
  const timer = setTimeout(() => timeout.abort(new DOMException("Timeout elapsed", "TimeoutError")), ctx?.timeoutMs ?? 200_000);
470
470
  const callerSignal = ctx?.abortSignal ?? new AbortController().signal;
471
471
  try {
472
- return await executor(request.url, {
472
+ return await (ctx?.executor ?? executor)(request.url, {
473
473
  method: request.method,
474
474
  headers: request.headers,
475
475
  body: request.body,
@@ -217,7 +217,13 @@ export type RoutingCommentaryDecision =
217
217
  | { kind: "flush" }
218
218
  | { kind: "hallucination" };
219
219
 
220
- const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b/giu;
220
+ // The Korean alternative is deliberately asymmetric: it has a left boundary and no right one.
221
+ // Korean attaches particles directly to the noun, so the real sentences this detector exists to
222
+ // catch read "네이티브 셸이 차단되어..." and "네이티브 셸과 Read가...". A mirrored
223
+ // (?![\p{L}\p{M}\p{N}_]) lookahead would see the 이/과 particle as a letter and stop matching
224
+ // every one of them, which is why the negative cases below only probe the left side. Adding the
225
+ // right boundary looks like an obvious fix and disables the check; do not.
226
+ const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b|(?<![\p{L}\p{M}\p{N}_])네이티브\s*(?:셸|쉘)/giu;
221
227
  const ROUTING_TOOL_HINT =
222
228
  /(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
223
229
  const ROUTING_FAILURE_CLAIM =
@@ -276,7 +282,7 @@ export class CursorRoutingCommentarySniffer {
276
282
  private matchesHallucination(): boolean {
277
283
  if (!ROUTING_FAILURE_CLAIM.test(this.buffered)) return false;
278
284
  const nativeTools = new Set(
279
- [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
285
+ [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase() ?? "shell"),
280
286
  );
281
287
  if (nativeTools.size === 0) return false;
282
288
  return ROUTING_REDIRECT_CLAIM.test(this.buffered) || nativeTools.size >= 2;
@@ -796,14 +796,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
796
796
  // body, URL or credential.
797
797
  const requestedTextFormat = parsed.options.textFormat;
798
798
  if (requestedTextFormat) {
799
- if (provider.googleMode === "cloud-code-assist") {
800
- // Not implemented or verified by opencodex for the Cloud Code Assist envelope,
801
- // including Claude models served through it. This is not a claim that the
802
- // upstream cannot do it — silence would return unconstrained prose as success,
803
- // which is the failure this fix exists to remove.
799
+ if (provider.googleMode === "cloud-code-assist" && !parsed.modelId.startsWith("gemini-")) {
800
+ // Not implemented by opencodex for non-Gemini models (including Claude)
801
+ // served through the Cloud Code Assist envelope. This is not a claim that
802
+ // the upstream cannot do it — silence would return unconstrained prose as success,
803
+ // which is the failure this refusal exists to prevent.
804
804
  throw new Error(
805
- "google cloud-code-assist structured output is not implemented by opencodex — "
806
- + "remove response_format or route this model through AI Studio or Vertex",
805
+ "google cloud-code-assist structured output is not implemented by opencodex for non-Gemini models — "
806
+ + "remove response_format or route this model through a direct provider",
807
807
  );
808
808
  }
809
809
  if (isImageCapableModel(parsed.modelId)) {
@@ -38,7 +38,12 @@ import {
38
38
  validateKiroConversationState,
39
39
  type KiroTurn,
40
40
  } from "./conversation";
41
- import { injectKiroThinkingTags, kiroNativeEffortField, KIRO_NATIVE_EFFORTS } from "./reasoning";
41
+ import {
42
+ injectKiroThinkingTags,
43
+ kiroNativeEffortField,
44
+ kiroReasoningContent,
45
+ KIRO_NATIVE_EFFORTS,
46
+ } from "./reasoning";
42
47
  import { kiroPayloadMessages, userContentText } from "./usage";
43
48
  import {
44
49
  kiroToolWireNames,
@@ -388,7 +393,11 @@ export function buildKiroPayload(
388
393
  assistantResponseMessage: {
389
394
  content: turn.content,
390
395
  ...(turn.toolUses.length > 0 ? { toolUses: turn.toolUses } : {}),
391
- ...(turn.redactedReasoning ? { reasoningContent: { redactedContent: turn.redactedReasoning } } : {}),
396
+ // Replayed on the field it was received on: the GPT-5.6 signature is not base64 and is
397
+ // rejected when sent as `redactedContent`.
398
+ ...(turn.redactedReasoning
399
+ ? { reasoningContent: kiroReasoningContent(turn.redactedReasoning) }
400
+ : {}),
392
401
  },
393
402
  }
394
403
  : {
@@ -447,7 +456,12 @@ export function buildKiroPayload(
447
456
  if (!KIRO_NATIVE_EFFORTS.includes(effort)) {
448
457
  throw new Error(`Kiro ${normalizeKiroModelId(parsed.modelId)} does not support reasoning effort ${JSON.stringify(effort)}`);
449
458
  }
450
- payload.additionalModelRequestFields = { [effortField]: { effort } };
459
+ // Model eligibility still owns unsupported-effort validation above; wire eligibility
460
+ // is narrower for luna/terra, whose unverified rungs retain the thinking-tag path.
461
+ const verifiedEffortField = kiroNativeEffortField(parsed.modelId, effort);
462
+ if (verifiedEffortField) {
463
+ payload.additionalModelRequestFields = { [verifiedEffortField]: { effort } };
464
+ }
451
465
  }
452
466
  if (profileArn) payload.profileArn = profileArn;
453
467
  return { payload, nameMap, conversationId, completionMode };
@@ -4,21 +4,46 @@ import type { OcxParsedRequest } from "../../types";
4
4
  export type KiroReasoningMode = "native" | "emulated";
5
5
 
6
6
  // Kiro takes a verified native effort field for these models, and each model family names it
7
- // differently: the Sol-only `reasoning.effort` versus the Claude-specific `output_config.effort`.
8
- // Models absent from this table fall back to emulated thinking instructions.
7
+ // differently: the GPT-5.6 family's `reasoning.effort` versus the Claude-specific
8
+ // `output_config.effort`. Models absent from this table fall back to emulated thinking
9
+ // instructions.
10
+ //
11
+ // The GPT-5.6 entries are measured against the live runtime rather than inferred from the vendor
12
+ // schema: the field is accepted (HTTP 200) and the encrypted reasoning blob that comes back grows
13
+ // with the effort. On one fixed hard prompt — a primality search plus a 20-bit recurrence count —
14
+ // luna's blob measured 5,130 chars at `low`, 16,686 at `medium`, 30,670 at `high` and 48,594 at
15
+ // `max`, against 13,118 with no effort signal at all; terra's measured 34,590 and 38,106 at native
16
+ // `max` against 11,758 and 17,598 bare, two repetitions each. The channel this replaces — the
17
+ // emulated `<thinking_mode>` tag block, which was all those models used to receive — measured
18
+ // 21,202 (`low`) and 28,302 (`max`) for luna, i.e. between that model's native `medium` and
19
+ // `high`, never reaching native `max`. `gpt-5.6-sol`'s native `max` cross-checked at 30,498 on the
20
+ // same prompt. Terra's absence from this table was therefore an omission rather than a capability
21
+ // difference: what the earlier Sol-only scope recorded was not reproducible here.
9
22
  export const KIRO_NATIVE_EFFORT_FIELDS: Record<string, "reasoning" | "output_config"> = {
10
23
  "gpt-5.6-sol": "reasoning",
24
+ "gpt-5.6-terra": "reasoning",
25
+ "gpt-5.6-luna": "reasoning",
11
26
  "claude-opus-5": "output_config",
12
27
  };
13
28
 
14
29
  export const KIRO_NATIVE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
15
30
 
16
- export function kiroNativeEffortField(modelId: string): "reasoning" | "output_config" | undefined {
17
- return KIRO_NATIVE_EFFORT_FIELDS[normalizeKiroModelId(modelId)];
31
+ // The newly enabled models have evidence for these rungs only. Keep the previous
32
+ // emulation for xhigh, and never widen their native wire when the shared ladder grows.
33
+ const KIRO_LUNA_TERRA_NATIVE_EFFORTS = new Set(["low", "medium", "high", "max"]);
34
+
35
+ export function kiroNativeEffortField(
36
+ modelId: string,
37
+ effort?: string,
38
+ ): "reasoning" | "output_config" | undefined {
39
+ const model = normalizeKiroModelId(modelId);
40
+ if ((model === "gpt-5.6-luna" || model === "gpt-5.6-terra")
41
+ && effort !== undefined && !KIRO_LUNA_TERRA_NATIVE_EFFORTS.has(effort)) return undefined;
42
+ return KIRO_NATIVE_EFFORT_FIELDS[model];
18
43
  }
19
44
 
20
- export function kiroReasoningMode(modelId: string): KiroReasoningMode {
21
- return kiroNativeEffortField(modelId) ? "native" : "emulated";
45
+ export function kiroReasoningMode(modelId: string, effort?: string): KiroReasoningMode {
46
+ return kiroNativeEffortField(modelId, effort) ? "native" : "emulated";
22
47
  }
23
48
 
24
49
  export function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
@@ -38,7 +63,7 @@ export function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined
38
63
  }
39
64
 
40
65
  export function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest): string {
41
- if (kiroReasoningMode(parsed.modelId) !== "emulated") return content;
66
+ if (kiroReasoningMode(parsed.modelId, parsed.options.reasoning) !== "emulated") return content;
42
67
  const budget = kiroThinkingBudget(parsed);
43
68
  if (!budget) return content;
44
69
  const instruction = [
@@ -54,3 +79,41 @@ export function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest
54
79
  content,
55
80
  ].join("\n");
56
81
  }
82
+
83
+ /**
84
+ * The blob from a Kiro `reasoningContentEvent` has two possible homes on a replayed assistant
85
+ * turn, and the wire validates the SHAPE of each rather than its content: `signature` takes the
86
+ * emitted string verbatim, while `redactedContent` is a base64 member. The `.KTR~~…` value every
87
+ * GPT-5.6 capture returns is NOT valid base64, which is exactly why replaying it as
88
+ * `redactedContent` — what this proxy did before the field was measured — came back as
89
+ * REQUEST_BODY_INVALID ("Improperly formed request").
90
+ *
91
+ * The blob travels as ONE opaque string: adapter event, `ocxr1:` reasoning envelope, then
92
+ * `OcxAssistantMessage.kiroRedactedReasoning`. The field it arrived on therefore rides that same
93
+ * string, instead of a second parallel value that could drift from it. Provider data cannot forge
94
+ * the tag: the other channel is base64, whose alphabet has no colon.
95
+ */
96
+ export const KIRO_REASONING_SIGNATURE_TAG = "signature:";
97
+
98
+ export function tagKiroReasoningBlob(field: "signature" | "redactedContent", data: string): string {
99
+ return field === "signature" ? KIRO_REASONING_SIGNATURE_TAG + data : data;
100
+ }
101
+
102
+ /** The wire field a stored blob arrived on, and its untagged value. */
103
+ export function splitKiroReasoningBlob(value: string): { field: "signature" | "redactedContent"; data: string } {
104
+ return value.startsWith(KIRO_REASONING_SIGNATURE_TAG)
105
+ ? { field: "signature", data: value.slice(KIRO_REASONING_SIGNATURE_TAG.length) }
106
+ : { field: "redactedContent", data: value };
107
+ }
108
+
109
+ /**
110
+ * The `reasoningContent` object on an `assistantResponseMessage`. Exactly one member is set: the
111
+ * wire validates the shape, so the two cannot be substituted for each other.
112
+ */
113
+ export type KiroReasoningContent = { signature: string } | { redactedContent: string };
114
+
115
+ /** `reasoningContent` for a replayed `assistantResponseMessage`, carrying the blob verbatim. */
116
+ export function kiroReasoningContent(value: string): KiroReasoningContent {
117
+ const { field, data } = splitKiroReasoningBlob(value);
118
+ return field === "signature" ? { signature: data } : { redactedContent: data };
119
+ }
@@ -19,6 +19,7 @@ import { noteKiroTransientThrottle } from "../kiro-retry";
19
19
  import { KiroThinkingParser } from "../kiro-thinking";
20
20
  import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation";
21
21
  import { isValidKiroConversationId } from "../kiro-wire";
22
+ import { tagKiroReasoningBlob } from "./reasoning";
22
23
  import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage";
23
24
 
24
25
  // Stream parsing (shared by parseStream + parseResponse)
@@ -633,8 +634,13 @@ async function* parseKiroAttemptEvents(
633
634
  if (ev.data) {
634
635
  yield* emitRetained(stage({ type: "reasoning_raw_delta", text: ev.data }));
635
636
  }
636
- if (ev.redactedContent) {
637
- yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: ev.redactedContent }));
637
+ // The blob is replayed on the field it arrived on, so remember that field here — this is
638
+ // the only place that still knows it. See kiro/reasoning.ts for why the distinction is
639
+ // load-bearing rather than cosmetic.
640
+ if (ev.signature) {
641
+ yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: tagKiroReasoningBlob("signature", ev.signature) }));
642
+ } else if (ev.redactedContent) {
643
+ yield* emitRetained(stage({ type: "kiro_redacted_reasoning", data: tagKiroReasoningBlob("redactedContent", ev.redactedContent) }));
638
644
  }
639
645
  break;
640
646
  case "context_usage":
@@ -1,5 +1,6 @@
1
1
  import type { OcxProviderConfig } from "../../types";
2
2
  import type { KiroImage } from "../kiro-images";
3
+ import type { KiroReasoningContent } from "./reasoning";
3
4
 
4
5
  export const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
5
6
  export const SDK_VERSION = "1.0.27";
@@ -51,7 +52,7 @@ export interface KiroHistoryEntry {
51
52
  assistantResponseMessage?: {
52
53
  content: string;
53
54
  toolUses?: KiroToolUse[];
54
- reasoningContent?: { redactedContent: string };
55
+ reasoningContent?: KiroReasoningContent;
55
56
  };
56
57
  }
57
58
 
@@ -3,7 +3,7 @@ import { kiroTruncationReason } from "./kiro-truncation";
3
3
 
4
4
  export type ParsedKiroEvent =
5
5
  | { type: "content"; data?: string; modelId?: string }
6
- | { type: "reasoning"; data?: string; redactedContent?: string }
6
+ | { type: "reasoning"; data?: string; signature?: string; redactedContent?: string }
7
7
  | { type: "context_usage"; contextUsagePercentage: number }
8
8
  | { type: "tool"; name?: string; toolUseId?: string; input?: string; stop?: boolean }
9
9
  | { type: "truncation"; data: string }
@@ -138,18 +138,26 @@ export function parseKiroEvent(eventType: string, payload: Uint8Array): ParsedKi
138
138
  : {}),
139
139
  };
140
140
  case "reasoningContentEvent":
141
- // `text` is plaintext reasoning; `redactedContent` is the encrypted blob the GPT-5.6 family
142
- // (sol/terra/luna) actually returns — they never send `text`. Keyed off the wire field, not
143
- // the model id. Both may be absent on a bare event.
144
- return {
145
- type: "reasoning",
146
- ...(optionalString(eventType, parsed, "text") !== undefined
147
- ? { data: optionalString(eventType, parsed, "text") }
148
- : {}),
149
- ...(optionalString(eventType, parsed, "redactedContent") !== undefined
150
- ? { redactedContent: optionalString(eventType, parsed, "redactedContent") }
151
- : {}),
152
- };
141
+ // `text` is plaintext reasoning; the GPT-5.6 family (sol/terra/luna) instead returns an
142
+ // encrypted blob, and the field it arrives on has to be replayed unchanged (see
143
+ // kiro/reasoning.ts): `signature` carries the `.KTR~~…` value verbatim and is what every
144
+ // capture of those models sent, while `redactedContent` — the base64 shape a capture has
145
+ // never shown — stays accepted for any model that sends it. Keyed off the wire field, not the
146
+ // model id. Any of the three may be absent on a bare event.
147
+ {
148
+ const text = optionalString(eventType, parsed, "text");
149
+ const signature = optionalString(eventType, parsed, "signature");
150
+ const redacted = optionalString(eventType, parsed, "redactedContent");
151
+ return {
152
+ type: "reasoning",
153
+ ...(text !== undefined ? { data: text } : {}),
154
+ ...(signature !== undefined
155
+ ? { signature }
156
+ : redacted !== undefined
157
+ ? { redactedContent: redacted }
158
+ : {}),
159
+ };
160
+ }
153
161
  case "toolUseEvent":
154
162
  return {
155
163
  type: "tool",