@coseung2/opencodex 2.8.0-cs.15 → 2.8.0-cs.17

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 (61) hide show
  1. package/gui/dist/assets/{index-BhXIu7c0.js → index-Ch-99jy3.js} +2 -2
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/packages/ocx-notch/README.md +3 -1
  5. package/src/adapters/base.ts +12 -0
  6. package/src/adapters/identity.ts +1 -1
  7. package/src/adapters/kiro-calibration.ts +83 -0
  8. package/src/adapters/kiro-constants.ts +11 -2
  9. package/src/adapters/kiro-errors.ts +11 -0
  10. package/src/adapters/kiro-events.ts +19 -1
  11. package/src/adapters/kiro-thinking.ts +18 -2
  12. package/src/adapters/kiro-tools.ts +12 -3
  13. package/src/adapters/kiro.ts +300 -78
  14. package/src/adapters/openai-chat.ts +1 -42
  15. package/src/adapters/openai-responses.ts +126 -9
  16. package/src/adapters/xai-schema-analysis.ts +78 -0
  17. package/src/adapters/xai-tool-schema.ts +274 -0
  18. package/src/adapters/xai-web-search.ts +138 -0
  19. package/src/bridge.ts +61 -6
  20. package/src/cli/observe.ts +18 -3
  21. package/src/codex/app-server-processes.ts +3 -5
  22. package/src/codex/catalog/effort.ts +4 -2
  23. package/src/codex/catalog/metadata.ts +42 -9
  24. package/src/codex/catalog/parsing.ts +17 -2
  25. package/src/codex/catalog/provider-fetch.ts +9 -3
  26. package/src/codex/catalog/sync.ts +11 -5
  27. package/src/codex/data/upstream-models.json +169 -0
  28. package/src/grok/inject.ts +1 -1
  29. package/src/lib/errors.ts +18 -0
  30. package/src/lib/token-estimate.ts +42 -38
  31. package/src/lib/translator-budget.ts +34 -0
  32. package/src/oauth/index.ts +10 -4
  33. package/src/oauth/kiro.ts +71 -6
  34. package/src/oauth/store.ts +3 -1
  35. package/src/oauth/types.ts +4 -0
  36. package/src/providers/derive.ts +7 -5
  37. package/src/providers/opencode-go-transport.ts +59 -0
  38. package/src/providers/quota.ts +68 -60
  39. package/src/providers/registry.ts +41 -10
  40. package/src/providers/xai-transport.ts +10 -0
  41. package/src/responses/compaction.ts +8 -1
  42. package/src/responses/namespace-aliases.ts +56 -0
  43. package/src/responses/parser.ts +12 -0
  44. package/src/responses/reasoning-envelope.ts +9 -1
  45. package/src/responses/snapshot-policy.ts +108 -0
  46. package/src/responses/state.ts +23 -10
  47. package/src/responses/turn-termination.ts +108 -0
  48. package/src/responses/xai-custom-tool-compat.ts +237 -0
  49. package/src/server/grok-responses-snapshot-repair.ts +338 -0
  50. package/src/server/index.ts +2 -1
  51. package/src/server/relay-eager.ts +1 -0
  52. package/src/server/request-log-conversation.ts +8 -0
  53. package/src/server/request-log.ts +5 -4
  54. package/src/server/responses/core.ts +233 -16
  55. package/src/server/responses-image-gen-repair.ts +2 -2
  56. package/src/server/sse-payload-rewrite.ts +20 -3
  57. package/src/types.ts +10 -1
  58. package/src/usage/cost.ts +0 -0
  59. package/src/usage/expected-prices.ts +7 -0
  60. package/src/usage/log.ts +1 -2
  61. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
@@ -9,12 +9,14 @@ import {
9
9
  import { parseRequest } from "../../responses/parser";
10
10
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
11
11
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
12
+ import { XaiToolSchemaCompatibilityError } from "../../adapters/xai-tool-schema";
12
13
  import {
13
14
  expandPreviousResponseInput,
14
15
  previousResponseProviderState,
15
16
  previousResponseReplayFailure,
16
17
  rememberResponseState,
17
18
  } from "../../responses/state";
19
+ import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../responses/turn-termination";
18
20
  import { routeModel, type RouteResult } from "../../router";
19
21
  import {
20
22
  advanceComboAfterFailure,
@@ -100,11 +102,17 @@ import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve"
100
102
  import type { InboundWire } from "../../providers/registry";
101
103
  import { hasKeyPoolFailover, rotateProviderTransportOn429 } from "../../providers/key-failover";
102
104
  import { shouldAttemptImageTierRetry } from "../image-retry";
103
- import { resolveProviderTransport } from "../../providers/xai-transport";
105
+ import { isXaiResponsesDestination, resolveProviderTransport } from "../../providers/xai-transport";
106
+ import { isOpenCodeMuseResponses, resolveOpenCodeGoTransport } from "../../providers/opencode-go-transport";
107
+ import { declaredNamespaceAliases } from "../../responses/namespace-aliases";
104
108
  import type { WsData } from "../ws-bridge";
105
109
  import { trackActiveTurnLease, trackStreamLifetime } from "../lifecycle";
106
110
  import { redactSecretString } from "../../lib/redact";
107
111
  import { readBoundedResponseBody } from "../../lib/bounded-body";
112
+ import {
113
+ isRateLimitOrQuotaFailureMessage,
114
+ upstreamErrorMessageFromPayload,
115
+ } from "../../lib/errors";
108
116
  import type { AdmissionLease } from "../../lib/admission";
109
117
  import { supportedLadderFor } from "../effort-policy";
110
118
  import { isThreadSpawnRequest } from "../effort-policy";
@@ -130,6 +138,7 @@ import {
130
138
  import {
131
139
  conversationIdFromResponsesRequest,
132
140
  normalizeLogConversationId,
141
+ sessionLaneIdFromRequest,
133
142
  sessionIdHeaderFromRequest,
134
143
  } from "../request-log-conversation";
135
144
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -152,12 +161,18 @@ import {
152
161
  createResponsesItemIdPayloadRewrite,
153
162
  hasResponsesItemIdRepair,
154
163
  } from "../responses-item-id-repair";
164
+ import { createGrokResponsesSparseTerminalPayloadRewrite } from "../grok-responses-snapshot-repair";
155
165
  import {
156
166
  createImageGenCallRestoreRewrite,
157
167
  imageGenToolCallAliases,
158
168
  restoreImageGenCallsInJson,
159
169
  } from "../responses-image-gen-repair";
160
170
  import { composeSsePayloadRewrites, relaySseWithPayloadRewrite } from "../sse-payload-rewrite";
171
+ import {
172
+ createXaiCustomToolPayloadRewrite,
173
+ restoreXaiCustomCallsInJson,
174
+ xaiResponsesCustomToolNames,
175
+ } from "../../responses/xai-custom-tool-compat";
161
176
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
162
177
 
163
178
  import { buildToolBridgeMaps, collabSurface, injectDeveloperMessage, multiAgentGuidanceText } from "./collaboration";
@@ -252,8 +267,40 @@ async function shouldRetryCodexPoolAccountModel400(
252
267
  }
253
268
 
254
269
  /** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
255
- function shouldRetryCodexPoolAccountQuota(response: Response): boolean {
256
- return response.status === 429 || response.status === 402;
270
+ function codexQuotaFailureMessage(body: string): string | undefined {
271
+ try {
272
+ const payload = JSON.parse(body) as unknown;
273
+ const canonical = upstreamErrorMessageFromPayload(payload);
274
+ if (canonical !== undefined) return canonical;
275
+ if (typeof payload === "string") return payload;
276
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
277
+ const record = payload as Record<string, unknown>;
278
+ if (typeof record.message === "string") return record.message;
279
+ return typeof record.error === "string" ? record.error : undefined;
280
+ } catch {
281
+ // Plain-text gateways remain supported. Valid JSON is inspected only at recognized
282
+ // message fields so echoed request content elsewhere cannot trigger account cooldown.
283
+ return body;
284
+ }
285
+ }
286
+
287
+ export async function shouldRetryCodexPoolAccountQuota(
288
+ response: Response,
289
+ signal?: AbortSignal,
290
+ ): Promise<boolean> {
291
+ if (response.status === 402 || response.status === 429) return true;
292
+ if (response.status < 500 || response.status >= 600) return false;
293
+ try {
294
+ // Reject malformed UTF-8 instead of matching quota words around replacement characters.
295
+ const body = await readBoundedResponseBody(response.clone(), { signal, fatalUtf8: true });
296
+ const message = body.displaySafe && !body.truncated
297
+ ? codexQuotaFailureMessage(body.text)
298
+ : undefined;
299
+ return message !== undefined
300
+ && isRateLimitOrQuotaFailureMessage(message);
301
+ } catch {
302
+ return false;
303
+ }
257
304
  }
258
305
 
259
306
  interface CodexPoolAccountRetryArgs {
@@ -351,6 +398,19 @@ async function retryCodexPoolOnAlternateAccount(
351
398
  ) throw error;
352
399
  }
353
400
  if (retryAuthCtx?.kind !== "pool" && retryAuthCtx?.kind !== "main-pool") {
401
+ // A body-confirmed quota response may arrive under HTTP 5xx. Without an alternate,
402
+ // the ordinary terminal recorder sees only that wire status and would misclassify it
403
+ // as transient, leaving the exhausted account immediately selectable next turn.
404
+ if (outcomeStatus !== firstResponse.status && (outcomeStatus === 429 || outcomeStatus === 402)) {
405
+ recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
406
+ ...codexQuotaOutcomeMeta(firstResponse),
407
+ threadId: req.headers.get("x-codex-parent-thread-id"),
408
+ modelId: route.modelId,
409
+ probeLeaseId: codexProbeLeaseId(firstAuthCtx),
410
+ probeQuotaScope: codexProbeQuotaScope(firstAuthCtx),
411
+ writerGeneration: firstAuthCtx.writerGeneration,
412
+ });
413
+ }
354
414
  return { kind: "no-alternate" };
355
415
  }
356
416
 
@@ -826,6 +886,7 @@ async function applyFinalRouteRequestNormalization(args: {
826
886
  }
827
887
  // Settle the wire once so logging, fast-mode, auth, and sidecars read the adapter
828
888
  // this request will actually use (#404).
889
+ route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers));
829
890
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
830
891
  logCtx.model = route.modelId;
831
892
  logCtx.provider = route.providerName;
@@ -837,8 +898,11 @@ async function applyFinalRouteRequestNormalization(args: {
837
898
  // Virtual model rewriting: Pro aliases → base model + reasoning.mode="pro".
838
899
  applyOpenAiVirtualModel(parsed, route, logCtx);
839
900
 
840
- // Fast mode override for OpenAI-routed models.
841
- if (config.fastMode !== undefined && route.provider.adapter === "openai-responses") {
901
+ // Fast mode override for OpenAI-shaped routes. xAI OAuth's subscription Responses lane has no
902
+ // caller-owned Priority/Fast contract, so switching Grok to its native wire must not make a
903
+ // global OpenAI fastMode leak service_tier upstream.
904
+ const callerFastTierAllowed = !(route.providerName === "xai" && route.provider.authMode === "oauth");
905
+ if (config.fastMode !== undefined && route.provider.adapter === "openai-responses" && callerFastTierAllowed) {
842
906
  const tier = config.fastMode ? "priority" : undefined;
843
907
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
844
908
  if (tier) (parsed._rawBody as Record<string, unknown>).service_tier = tier;
@@ -1278,6 +1342,7 @@ async function handleResponsesInner(
1278
1342
  cursorConversationId: parsed._cursorConversationId,
1279
1343
  });
1280
1344
  }
1345
+ bindTurnTerminationScope(parsed, logCtx.conversationId);
1281
1346
  logCtx.requestedModel = parsed.modelId;
1282
1347
  logCtx.requestedEffort = parsed.options.reasoning;
1283
1348
  logCtx.requestedServiceTier = parsed.options.serviceTier;
@@ -1584,9 +1649,6 @@ async function handleResponsesInner(
1584
1649
  }
1585
1650
 
1586
1651
  if ("passthrough" in adapter && adapter.passthrough && !routedCompaction) {
1587
- const imageGenCallAliases = route.provider.authMode === "forward"
1588
- ? new Map<string, { namespace: string; name: string }>()
1589
- : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
1590
1652
  // Local continuation cache for the ChatGPT passthrough. Codex WS turns chain with
1591
1653
  // previous_response_id, ocx converts them to internal HTTP requests, and the ChatGPT Codex
1592
1654
  // REST backend rejects the parameter — the adapter strips it in forward mode, so the ONLY
@@ -1608,7 +1670,27 @@ async function handleResponsesInner(
1608
1670
  + `(model ${parsed.modelId}); forwarding without it — earlier turns may be missing from this request`,
1609
1671
  );
1610
1672
  }
1611
- let request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1673
+ let request: Awaited<ReturnType<typeof adapter.buildRequest>>;
1674
+ try {
1675
+ request = await adapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1676
+ } catch (error) {
1677
+ releaseCodexAuthContextProbeLease(authCtx);
1678
+ if (error instanceof XaiToolSchemaCompatibilityError) {
1679
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(error.message));
1680
+ }
1681
+ throw error;
1682
+ }
1683
+ const xaiCustomToolNames = isXaiResponsesDestination(route.provider)
1684
+ ? xaiResponsesCustomToolNames(parsed._rawBody)
1685
+ : new Set<string>();
1686
+ // Reuse the existing client-only namespace rewrite and its bounded SSE relay. The
1687
+ // inspection/cache branch retains raw names for continuation replay. Resolve all
1688
+ // declaration ownership before admitting a Muse dotted alias.
1689
+ const imageGenCallAliases = route.provider.authMode === "forward"
1690
+ ? new Map<string, { namespace: string; name: string }>()
1691
+ : isOpenCodeMuseResponses(parsed.modelId, request.url)
1692
+ ? declaredNamespaceAliases(parsed.context.tools ?? [], translatorBudget)
1693
+ : imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
1612
1694
  recordAdapterReasoning(logCtx, request);
1613
1695
  const passthroughEstimate = typeof request.usageLog?.inputTokens === "number"
1614
1696
  ? request.usageLog.inputTokens
@@ -1663,6 +1745,66 @@ async function handleResponsesInner(
1663
1745
  request.releaseBodyObservation?.();
1664
1746
  }
1665
1747
 
1748
+ // Native Responses providers return from this passthrough branch before the generic recovery
1749
+ // loop below. Preserve the same OAuth contract here: one pre-stream 401 forces one credential
1750
+ // refresh and one rebuilt replay. This is required for xAI now that Grok 4.6/4.5 subscription
1751
+ // traffic uses the provider's native Responses wire.
1752
+ if (upstreamResponse.status === 401 && isOAuth401ReplayProvider && sentOAuthSnapshot) {
1753
+ try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed */ }
1754
+ let refreshed: OAuthAccessSnapshot;
1755
+ try {
1756
+ refreshed = await forceRefreshOAuthAccessSnapshot(sentOAuthSnapshot);
1757
+ } catch (err) {
1758
+ upstream.abort();
1759
+ releaseCodexAuthContextProbeLease(authCtx);
1760
+ return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
1761
+ }
1762
+ sentOAuthSnapshot = refreshed;
1763
+ if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
1764
+ const refreshedProvider = resolveProviderTransport(
1765
+ route.providerName,
1766
+ { ...route.provider, apiKey: refreshed.accessToken },
1767
+ parsed.options.promptCacheKey,
1768
+ route.providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(route.providerName) : undefined,
1769
+ );
1770
+ route.provider = refreshedProvider;
1771
+ const refreshedAdapter = resolveAdapter(
1772
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
1773
+ config.cacheRetention,
1774
+ );
1775
+ if (!("passthrough" in refreshedAdapter) || !refreshedAdapter.passthrough) {
1776
+ upstream.abort();
1777
+ return formatErrorResponse(502, "upstream_error", "OAuth refresh changed the provider wire unexpectedly");
1778
+ }
1779
+ logCtx.providerAdapter = refreshedAdapter.name;
1780
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, refreshedAdapter.name, logCtx.accountLogLabel);
1781
+ try {
1782
+ request = await refreshedAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
1783
+ recordAdapterReasoning(logCtx, request);
1784
+ } catch (err) {
1785
+ upstream.abort();
1786
+ if (options.abortSignal?.aborted) return clientCancelledResponse();
1787
+ return formatErrorResponse(400, "invalid_request_error", redactSecretString(err instanceof Error ? err.message : String(err)));
1788
+ }
1789
+ try {
1790
+ upstreamResponse = await fetchWithTransientRetry(
1791
+ recovery => {
1792
+ noteAttemptSend(logCtx.activeAttempt, passthroughEstimate, recovery ?? "oauth-401");
1793
+ return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
1794
+ method: request.method,
1795
+ headers: request.headers,
1796
+ body: request.body,
1797
+ }, recovery), upstream.signal, connectMs, parsed.stream, providerFetch(route.provider, options.codexWsRuntimeIdentity));
1798
+ },
1799
+ { abortSignal: upstream.signal, label: safeHostLabel(request.url) },
1800
+ );
1801
+ } catch (err) {
1802
+ return transportFailureResponse(err);
1803
+ } finally {
1804
+ request.releaseBodyObservation?.();
1805
+ }
1806
+ }
1807
+
1666
1808
  if (usesCodexForwardPoolAuth(authCtx, route.provider) && !authCtx.fixedAccount) {
1667
1809
  let poolRetryOutcome: number | undefined;
1668
1810
  if (await shouldRetryCodexPoolAccountModel400(
@@ -1671,9 +1813,14 @@ async function handleResponsesInner(
1671
1813
  options.abortSignal,
1672
1814
  )) {
1673
1815
  poolRetryOutcome = 400;
1674
- } else if (shouldRetryCodexPoolAccountQuota(upstreamResponse)) {
1816
+ } else if (!authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
1817
+ upstreamResponse,
1818
+ options.abortSignal,
1819
+ )) {
1675
1820
  // Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
1676
- poolRetryOutcome = upstreamResponse.status;
1821
+ // ChatGPT sometimes wraps quota exhaustion in a generic 5xx. Normalize only
1822
+ // body-confirmed cases to quota evidence so cooldown and rotation both apply.
1823
+ poolRetryOutcome = upstreamResponse.status >= 500 ? 429 : upstreamResponse.status;
1677
1824
  }
1678
1825
 
1679
1826
  if (poolRetryOutcome !== undefined) {
@@ -1808,10 +1955,20 @@ async function handleResponsesInner(
1808
1955
  // The bundled known-bad runtime remains on tee by default on both platforms.
1809
1956
  if (isEventStream && upstreamResponse.body) {
1810
1957
  const repairConfig = route.provider.responsesItemIdRepair;
1811
- const needsClientRewrite = imageGenCallAliases.size > 0 || hasResponsesItemIdRepair(repairConfig);
1812
- // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
1958
+ const xaiCustomToolRewrite = createXaiCustomToolPayloadRewrite(xaiCustomToolNames);
1959
+ const grokSparseTerminalRewrite = logCtx.surface === "grok"
1960
+ ? createGrokResponsesSparseTerminalPayloadRewrite(translatorBudget)
1961
+ : undefined;
1962
+ const needsClientRewrite = imageGenCallAliases.size > 0
1963
+ || hasResponsesItemIdRepair(repairConfig)
1964
+ || xaiCustomToolRewrite !== undefined
1965
+ || grokSparseTerminalRewrite !== undefined;
1966
+ // Compose opt-in payload rewrites into one parse/stringify pass. Provider-shape restoration
1967
+ // runs before generic item-id repair so the client sees the correct custom-tool identity.
1813
1968
  const payloadRewrites = [
1814
1969
  createImageGenCallRestoreRewrite(imageGenCallAliases),
1970
+ xaiCustomToolRewrite,
1971
+ grokSparseTerminalRewrite,
1815
1972
  hasResponsesItemIdRepair(repairConfig)
1816
1973
  ? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
1817
1974
  : undefined,
@@ -1974,7 +2131,8 @@ async function handleResponsesInner(
1974
2131
  rememberPassthroughResponse(JSON.parse(text) as { id?: unknown; output?: unknown; status?: unknown });
1975
2132
  } catch { /* non-JSON despite content-type; recording is best-effort */ }
1976
2133
  }
1977
- return new Response(restoreImageGenCallsInJson(text, imageGenCallAliases), {
2134
+ const restoredCustomTools = restoreXaiCustomCallsInJson(text, xaiCustomToolNames);
2135
+ return new Response(restoreImageGenCallsInJson(restoredCustomTools, imageGenCallAliases), {
1978
2136
  status: upstreamResponse.status,
1979
2137
  statusText: upstreamResponse.statusText,
1980
2138
  headers,
@@ -2333,6 +2491,62 @@ async function handleResponsesInner(
2333
2491
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2334
2492
  }
2335
2493
 
2494
+ // Some adapter inputs already contain the completed answer and require no inference at all.
2495
+ // Kiro uses this for replayed history ending in a delivered final answer. Keep this OUTSIDE the
2496
+ // ordinary empty-completion path: an outputless done is intentional here and must never trigger
2497
+ // another request for the closed task.
2498
+ const localTerminal = adapter.localTerminal?.(parsed);
2499
+ if (localTerminal) {
2500
+ const terminalEvents: AdapterEvent[] = [{
2501
+ type: "done",
2502
+ endTurn: true,
2503
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
2504
+ }];
2505
+ const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
2506
+ if (parsed.stream) {
2507
+ const localSse = bridgeToResponsesSSE(
2508
+ (async function* () { yield* terminalEvents; })(),
2509
+ parsed.modelId,
2510
+ toolNsMap,
2511
+ freeformToolNames,
2512
+ toolSearchToolNames,
2513
+ undefined,
2514
+ 2_000,
2515
+ {
2516
+ translatorBudget,
2517
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
2518
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
2519
+ onUsage: usage => {
2520
+ logCtx.usageFromBridge = true;
2521
+ if (usage) {
2522
+ logCtx.usage = usage;
2523
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2524
+ }
2525
+ },
2526
+ },
2527
+ );
2528
+ const localTurnAc = new AbortController();
2529
+ const tracked = trackStreamLifetime(localSse, localTurnAc, undefined, options.turnAdmissionLease);
2530
+ return new Response(tracked, {
2531
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" },
2532
+ });
2533
+ }
2534
+ const json = buildResponseJSON(terminalEvents, parsed.modelId, {
2535
+ translatorBudget,
2536
+ toolNsMap,
2537
+ freeformToolNames,
2538
+ toolSearchToolNames,
2539
+ onUsage: usage => {
2540
+ logCtx.usageFromBridge = true;
2541
+ if (usage) {
2542
+ logCtx.usage = usage;
2543
+ if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
2544
+ }
2545
+ },
2546
+ });
2547
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
2548
+ }
2549
+
2336
2550
  const upstream = new AbortController();
2337
2551
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
2338
2552
  const connectMs = config.connectTimeoutMs ?? 200_000;
@@ -2821,13 +3035,15 @@ async function handleResponsesInner(
2821
3035
  // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
2822
3036
  // giant stale chain Codex just replaced.
2823
3037
  ...(routedCompaction ? {} : {
2824
- onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) =>
3038
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
3039
+ if (activeAdapter.name === "kiro") rememberDeliveredFinalAnswer(parsed, response);
2825
3040
  rememberResponseState(
2826
3041
  parsed._rawBody,
2827
3042
  response,
2828
3043
  continuationStateForResponse(providerState),
2829
3044
  activeAdapter.name === "kiro" ? { force: true } : undefined,
2830
- ),
3045
+ );
3046
+ },
2831
3047
  }),
2832
3048
  },
2833
3049
  );
@@ -2887,6 +3103,7 @@ async function handleResponsesInner(
2887
3103
  });
2888
3104
  // See the streaming branch: compaction turns skip the continuation cache.
2889
3105
  if (!routedCompaction) {
3106
+ if (activeAdapter.name === "kiro") rememberDeliveredFinalAnswer(parsed, json);
2890
3107
  rememberResponseState(
2891
3108
  parsed._rawBody,
2892
3109
  json,
@@ -60,7 +60,7 @@ export function imageGenToolCallAliases(
60
60
  return aliases;
61
61
  }
62
62
 
63
- /** Recursively restore only exact image-gen function-call aliases in a parsed Responses payload. */
63
+ /** Restore exact client tool identities. Callers own destination-scoped alias admission. */
64
64
  function restoreImageGenCalls(
65
65
  value: unknown,
66
66
  aliases: ReadonlyMap<string, NamespacedTool>,
@@ -86,7 +86,7 @@ function restoreImageGenCalls(
86
86
 
87
87
  const name = typeof value.name === "string" ? value.name : undefined;
88
88
  const target = value.type === "function_call" && name ? aliases.get(name) : undefined;
89
- if (target) {
89
+ if (target && (value.namespace === undefined || value.namespace === target.namespace)) {
90
90
  restored.name = target.name;
91
91
  restored.namespace = target.namespace;
92
92
  changed = true;
@@ -7,7 +7,7 @@ import type { TranslatorBudget } from "../lib/translator-budget";
7
7
  * parse/stringify pass so a tee'd stream is not re-framed twice per event.
8
8
  */
9
9
 
10
- export type SsePayloadRewrite = (payload: string) => string;
10
+ export type SsePayloadRewrite = ((payload: string) => string) & { dispose?: () => void };
11
11
 
12
12
  /** Split one complete SSE event block while retaining its original blank-line delimiter. */
13
13
  export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
@@ -54,11 +54,19 @@ export function replaceSseDataPayload(block: string, payload: string): string {
54
54
  export function composeSsePayloadRewrites(...rewrites: SsePayloadRewrite[]): SsePayloadRewrite {
55
55
  if (rewrites.length === 0) return (payload) => payload;
56
56
  if (rewrites.length === 1) return rewrites[0]!;
57
- return (payload) => {
57
+ const composed = ((payload: string) => {
58
58
  let next = payload;
59
59
  for (const rewrite of rewrites) next = rewrite(next);
60
60
  return next;
61
- };
61
+ }) as SsePayloadRewrite;
62
+ if (rewrites.some(rewrite => rewrite.dispose !== undefined)) {
63
+ composed.dispose = () => {
64
+ for (const rewrite of rewrites) {
65
+ try { rewrite.dispose?.(); } catch { /* rewrite teardown is best-effort */ }
66
+ }
67
+ };
68
+ }
69
+ return composed;
62
70
  }
63
71
 
64
72
  /**
@@ -122,6 +130,12 @@ export function relaySseWithPayloadRewrite(
122
130
  buffer = "";
123
131
  bufferBytes = 0;
124
132
  };
133
+ let rewriteDisposed = false;
134
+ const disposeRewrite = (): void => {
135
+ if (rewriteDisposed) return;
136
+ rewriteDisposed = true;
137
+ try { rewrite.dispose?.(); } catch { /* stream teardown must not throw */ }
138
+ };
125
139
 
126
140
  const emitProcessedBlocks = (
127
141
  controller: ReadableStreamDefaultController<Uint8Array>,
@@ -156,6 +170,7 @@ export function relaySseWithPayloadRewrite(
156
170
  appendBuffer(decoder.decode());
157
171
  emitProcessedBlocks(controller, true);
158
172
  releaseBuffer();
173
+ disposeRewrite();
159
174
  controller.close();
160
175
  return;
161
176
  }
@@ -163,12 +178,14 @@ export function relaySseWithPayloadRewrite(
163
178
  emitProcessedBlocks(controller);
164
179
  } catch (error) {
165
180
  releaseBuffer();
181
+ disposeRewrite();
166
182
  try { await reader.cancel(error); } catch { /* already closed */ }
167
183
  controller.error(error);
168
184
  }
169
185
  },
170
186
  cancel(reason) {
171
187
  releaseBuffer();
188
+ disposeRewrite();
172
189
  reader.cancel(reason).catch(() => {});
173
190
  },
174
191
  });
package/src/types.ts CHANGED
@@ -28,7 +28,7 @@ export interface OcxParsedRequest {
28
28
  */
29
29
  _cursorIsolateConversation?: boolean;
30
30
  /** Account-scoped, non-secret Kiro request metadata selected with the OAuth access token. */
31
- _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
31
+ _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
32
32
  /** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
33
33
  _providerContinuation?: OcxProviderContinuationState;
34
34
  /**
@@ -85,6 +85,12 @@ export interface OcxAssistantMessage {
85
85
  phase?: OcxMessagePhase;
86
86
  model?: string;
87
87
  timestamp: number;
88
+ /**
89
+ * Kiro `reasoningContent.redactedContent` for THIS assistant turn — an opaque encrypted blob
90
+ * Kiro replays to preserve model reasoning across turns. Provider-specific and unrenderable, so
91
+ * it rides the message rather than a content part: any other adapter simply ignores it.
92
+ */
93
+ kiroRedactedReasoning?: string;
88
94
  }
89
95
 
90
96
  export interface OcxDeveloperMessage {
@@ -254,6 +260,9 @@ export type AdapterEvent =
254
260
  // opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400.
255
261
  | { type: "thinking_signature"; signature: string }
256
262
  | { type: "redacted_thinking"; data: string }
263
+ // Kiro reasoning round-trip: the encrypted `redactedContent` blob for the CURRENT assistant turn.
264
+ // Never rendered — it only rides the reasoning item's envelope so the next request can replay it.
265
+ | { type: "kiro_redacted_reasoning"; data: string }
257
266
  | { type: "reasoning_raw_delta"; text: string }
258
267
  | { type: "tool_call_start"; id: string; name: string }
259
268
  | { type: "tool_call_delta"; arguments: string }
package/src/usage/cost.ts CHANGED
Binary file
@@ -29,6 +29,9 @@ export interface ExpectedPriceOverlay {
29
29
  status: ExpectedPriceStatus;
30
30
  }
31
31
 
32
+ const GPT6_ASTRA: Cost4 = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
33
+ const ASTRA_API_PRICING = "https://developers.openai.com/api/docs/models/gpt-6-astra";
34
+
32
35
  const GEMINI_31_PRO: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 };
33
36
  const GEMINI_36_FLASH: Cost4 = { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 };
34
37
  const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 };
@@ -59,6 +62,9 @@ const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cac
59
62
  const QWEN38_ROUTEWAY_PRICING = "https://routeway.ai/models/qwen3.8-max-preview (temporary reseller proxy; NOT Alibaba Token Plan billing; cacheWrite unpublished -> 0)";
60
63
 
61
64
  export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
65
+ { provider: "openai-apikey", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: ASTRA_API_PRICING, verifiedAt: "2026-09-07", status: "verified" },
66
+ // Dollar display is an API-reference comparison, not a conversion of subscription credits.
67
+ { provider: "openai", modelId: "gpt-6-astra", cost4: GPT6_ASTRA, source: `API-reference comparison estimate: ${ASTRA_API_PRICING}`, verifiedAt: "2026-09-07", status: "verified-derived" },
62
68
  // claude-opus-5 is exposed by three providers but absent from the jawcode bundle, so
63
69
  // cost resolution returned null and the Logs `~$` column rendered an em dash. The
64
70
  // model-level vendor fallback only searches jawcode metadata, never overlays, so one
@@ -150,6 +156,7 @@ export function findExpectedPriceOverlay(
150
156
  * Models not listed here fall back to 1× (no multiplier).
151
157
  */
152
158
  export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
159
+ "gpt-6-astra": 2,
153
160
  "gpt-5.6-sol": 2,
154
161
  "gpt-5.6-terra": 1.6,
155
162
  "gpt-5.6-luna": 0.4,
package/src/usage/log.ts CHANGED
@@ -147,8 +147,7 @@ export function usageTotalTokens(usage: OcxUsage | undefined): number | undefine
147
147
  * fallback for paths that only know the configured provider name (e.g. "cursor-mykey").
148
148
  */
149
149
  function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
150
- return providerOrAdapter === "kiro" || providerOrAdapter.startsWith("kiro-")
151
- || providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
150
+ return providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
152
151
  }
153
152
 
154
153
  export function usageForFinalLog(provider: string, usage: OcxUsage | undefined): OcxUsage | undefined {