@bitkyc08/opencodex 2.33.0 → 2.34.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.
- package/README.md +3 -3
- package/gui/dist/assets/index-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +32 -4
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/types.ts +15 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +77 -11
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
package/src/adapters/google.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { identifyRoutedModel } from "./identity";
|
|
|
26
26
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
27
27
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
28
28
|
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
29
|
-
import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
29
|
+
import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
|
|
30
30
|
import {
|
|
31
31
|
isTranslatorBudgetExceededError,
|
|
32
32
|
retainTranslatedEventBatch,
|
|
@@ -48,6 +48,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
48
48
|
"- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
|
|
49
49
|
].join("\n");
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Documented output ceiling for a Google-surface model, or `undefined` when the id is not
|
|
53
|
+
* recognized.
|
|
54
|
+
*
|
|
55
|
+
* Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
|
|
56
|
+
* anything unmatched, which silently truncated aliases, gateway ids, and any model added after
|
|
57
|
+
* this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
|
|
58
|
+
* we cannot justify is worse than no cap: `structure/02_config-and-codex-home.md` is explicit
|
|
59
|
+
* that an explicit request value wins, so an unrecognized model passes through untouched and the
|
|
60
|
+
* upstream remains the authority on its own limit.
|
|
61
|
+
*
|
|
62
|
+
* Matching is prefix/family based rather than substring based for the same reason: `includes("pro")`
|
|
63
|
+
* matched any id containing "pro" (`my-prototype-model`), and `includes("oss")` matched any id
|
|
64
|
+
* containing "oss" (`crossover-v2`).
|
|
65
|
+
*/
|
|
66
|
+
export function maxOutputTokensForGoogleModel(modelId: string): number | undefined {
|
|
67
|
+
const lower = modelId.toLowerCase().trim();
|
|
68
|
+
if (lower.startsWith("gemini")) {
|
|
69
|
+
// Pro tops out one token below the flash/other Gemini ceiling; both are documented values.
|
|
70
|
+
return /(^|[-.])pro([-.]|$)/.test(lower) ? 65535 : 65536;
|
|
71
|
+
}
|
|
72
|
+
if (lower.startsWith("claude")) return 64000;
|
|
73
|
+
if (lower.startsWith("gpt-oss")) return 32768;
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function clampGoogleMaxOutputTokens(
|
|
78
|
+
modelId: string,
|
|
79
|
+
requestedTokens?: number,
|
|
80
|
+
): number | undefined {
|
|
81
|
+
if (requestedTokens === undefined || requestedTokens <= 0) return undefined;
|
|
82
|
+
const modelMax = maxOutputTokensForGoogleModel(modelId);
|
|
83
|
+
// Unknown model: honour the request as-is rather than inventing a ceiling for it.
|
|
84
|
+
if (modelMax === undefined) return requestedTokens;
|
|
85
|
+
return Math.min(requestedTokens, modelMax);
|
|
86
|
+
}
|
|
87
|
+
|
|
51
88
|
/**
|
|
52
89
|
* Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
|
|
53
90
|
* wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
|
|
@@ -141,7 +178,7 @@ function geminiTextPart(text: unknown): { text: string } | undefined {
|
|
|
141
178
|
*/
|
|
142
179
|
function geminiToolResultText(content: string | OcxContentPart[]): string {
|
|
143
180
|
if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
144
|
-
const hasContent = content.some(p => p.type
|
|
181
|
+
const hasContent = content.some(p => p.type !== "text" || p.text.length > 0);
|
|
145
182
|
return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
|
|
146
183
|
}
|
|
147
184
|
|
|
@@ -184,7 +221,7 @@ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
|
|
|
184
221
|
function messagesToGeminiFormat(
|
|
185
222
|
parsed: OcxParsedRequest,
|
|
186
223
|
identityModelId: string,
|
|
187
|
-
): { systemInstruction?: unknown; contents: unknown[] } {
|
|
224
|
+
): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
|
|
188
225
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
189
226
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
190
227
|
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
|
|
@@ -196,6 +233,7 @@ function messagesToGeminiFormat(
|
|
|
196
233
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
197
234
|
|
|
198
235
|
const contents: unknown[] = [];
|
|
236
|
+
const replayedCallIds: string[] = [];
|
|
199
237
|
|
|
200
238
|
const callIds = createToolCallIdAllocator();
|
|
201
239
|
for (const msg of parsed.context.messages) {
|
|
@@ -224,6 +262,13 @@ function messagesToGeminiFormat(
|
|
|
224
262
|
parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
|
|
225
263
|
continue;
|
|
226
264
|
}
|
|
265
|
+
if (p.type === "video") {
|
|
266
|
+
const data = parseDataUrl(p.videoUrl);
|
|
267
|
+
// Gemini accepts inline video bytes in the same Part union as images. Arbitrary
|
|
268
|
+
// remote URLs are not valid fileData references, so retain only a short marker.
|
|
269
|
+
parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
227
272
|
// Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
|
|
228
273
|
const textPart = geminiTextPart(p.text);
|
|
229
274
|
if (textPart) parts.push(textPart);
|
|
@@ -271,7 +316,10 @@ function messagesToGeminiFormat(
|
|
|
271
316
|
const signature = tc.providerMetadata?.google?.thoughtSignature
|
|
272
317
|
?? tc.thoughtSignature
|
|
273
318
|
?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
|
|
274
|
-
if (isLikelyRealThoughtSignature(signature))
|
|
319
|
+
if (isLikelyRealThoughtSignature(signature)) {
|
|
320
|
+
part.thoughtSignature = signature;
|
|
321
|
+
replayedCallIds.push(tc.id);
|
|
322
|
+
}
|
|
275
323
|
parts.push(part);
|
|
276
324
|
}
|
|
277
325
|
}
|
|
@@ -323,7 +371,7 @@ function messagesToGeminiFormat(
|
|
|
323
371
|
}
|
|
324
372
|
}
|
|
325
373
|
|
|
326
|
-
return { systemInstruction, contents };
|
|
374
|
+
return { systemInstruction, contents, replayedCallIds };
|
|
327
375
|
}
|
|
328
376
|
|
|
329
377
|
function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
|
|
@@ -610,6 +658,47 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
610
658
|
let vertexReplayModel: string | undefined;
|
|
611
659
|
let vertexReplaySession: string | undefined;
|
|
612
660
|
let restoreGoogleToolName = (name: string): string => name;
|
|
661
|
+
let lastInjectedCallIds: string[] = [];
|
|
662
|
+
let lastReasoningReplayScope: OcxParsedRequest["_reasoningReplayScope"];
|
|
663
|
+
|
|
664
|
+
// Conservative batch invalidation: upstream Gemini/Antigravity errors (e.g.
|
|
665
|
+
// "Function call is missing a thought_signature in functionCall parts") do not specify which
|
|
666
|
+
// specific call_id was rejected. When a request containing replayed signatures is rejected,
|
|
667
|
+
// we evict all callIds injected in that turn (lastInjectedCallIds) from the durable store
|
|
668
|
+
// and clear the session replay cache, preventing poisoned-signature loops while allowing
|
|
669
|
+
// subsequent turns to re-accumulate valid signatures. Unrelated calls from other turns remain intact.
|
|
670
|
+
//
|
|
671
|
+
// Memory-cache clearing stays broad (any invalid-argument/signature error can poison the
|
|
672
|
+
// session replay cache), but durable-store eviction is intentionally narrower: it only runs
|
|
673
|
+
// when the error text explicitly mentions a signature, so a generic tool-schema
|
|
674
|
+
// INVALID_ARGUMENT does not destroy valid durable signatures.
|
|
675
|
+
function handleSignatureRejection(errorMessage?: string) {
|
|
676
|
+
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
|
|
677
|
+
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
|
|
678
|
+
const text = errorMessage ?? "";
|
|
679
|
+
const isInvalidArgument = /invalid_argument|invalid argument/i.test(text);
|
|
680
|
+
const isSignatureError = /signature|thought_signature|thoughtSignature/i.test(text);
|
|
681
|
+
// The in-memory Antigravity replay cache only exists for CCA/Vertex, so clearing it stays
|
|
682
|
+
// scoped to those modes (replayModel/replaySession are undefined elsewhere anyway).
|
|
683
|
+
if (
|
|
684
|
+
(provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
685
|
+
&& replayModel && replaySession && (isInvalidArgument || isSignatureError)
|
|
686
|
+
) {
|
|
687
|
+
clearAntigravityReplay(replayModel, replaySession);
|
|
688
|
+
}
|
|
689
|
+
// The DURABLE store is not mode-scoped: signatures are remembered through
|
|
690
|
+
// rememberAndSerializeExtraContent and read back by lookupReplayThoughtSignature on every
|
|
691
|
+
// Google mode, including AI Studio. Gating eviction on CCA/Vertex therefore left AI Studio
|
|
692
|
+
// with rejected signatures cached forever, replaying them into every subsequent turn — the
|
|
693
|
+
// store poisons itself and the request keeps failing. Eviction follows the same scope the
|
|
694
|
+
// write does.
|
|
695
|
+
if (isSignatureError) {
|
|
696
|
+
for (const callId of lastInjectedCallIds) {
|
|
697
|
+
forgetThoughtSignatureForReplay(callId, lastReasoningReplayScope);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
613
702
|
return {
|
|
614
703
|
name: "google",
|
|
615
704
|
|
|
@@ -638,7 +727,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
638
727
|
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
|
|
639
728
|
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
|
|
640
729
|
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
|
|
641
|
-
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
|
|
730
|
+
const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(parsed, identityModelId);
|
|
731
|
+
lastInjectedCallIds = [...replayedCallIds];
|
|
732
|
+
lastReasoningReplayScope = parsed._reasoningReplayScope;
|
|
642
733
|
const tools = toolsToGeminiFormat(parsed);
|
|
643
734
|
|
|
644
735
|
const body: Record<string, unknown> = { contents };
|
|
@@ -650,7 +741,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
650
741
|
if (toolConfig) body.toolConfig = toolConfig;
|
|
651
742
|
|
|
652
743
|
const generationConfig: Record<string, unknown> = {};
|
|
653
|
-
|
|
744
|
+
const clampedMaxOutputTokens = clampGoogleMaxOutputTokens(identityModelId, parsed.options.maxOutputTokens);
|
|
745
|
+
if (clampedMaxOutputTokens !== undefined) generationConfig.maxOutputTokens = clampedMaxOutputTokens;
|
|
654
746
|
if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
|
|
655
747
|
if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
|
|
656
748
|
if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
|
|
@@ -864,14 +956,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
864
956
|
if (chunk.error) {
|
|
865
957
|
const err = chunk.error as { message?: string } | undefined;
|
|
866
958
|
// Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
|
|
867
|
-
// Drop the cache entry
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
|
|
871
|
-
&& replayModel && replaySession
|
|
872
|
-
&& /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
|
|
873
|
-
clearAntigravityReplay(replayModel, replaySession);
|
|
874
|
-
}
|
|
959
|
+
// Drop the cache entry and durable store entry for rejected calls so the next turn
|
|
960
|
+
// starts clean instead of re-injecting a bad sig.
|
|
961
|
+
handleSignatureRejection(err?.message);
|
|
875
962
|
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
876
963
|
return "terminate";
|
|
877
964
|
}
|
|
@@ -1158,7 +1245,18 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1158
1245
|
let raw: Record<string, unknown>;
|
|
1159
1246
|
let rawBytes = 0;
|
|
1160
1247
|
try {
|
|
1161
|
-
|
|
1248
|
+
const parsedRaw: unknown = JSON.parse(rawText);
|
|
1249
|
+
// `JSON.parse("null")` returns null instead of throwing, so the catch below cannot see it
|
|
1250
|
+
// and the `raw.error` read crashed the turn — #1219 at the buffered body root, which #1240
|
|
1251
|
+
// never reached because that audit swept SSE frame parsers only. There is no next frame to
|
|
1252
|
+
// recover into here, so unlike a stream frame this fails closed, matching the
|
|
1253
|
+
// unparseable-body branch just below and the buffered candidate guards added in #2232.
|
|
1254
|
+
if (!isGoogleRecord(parsedRaw)) {
|
|
1255
|
+
budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
|
|
1256
|
+
const valueType = googleStructuralValueType(parsedRaw);
|
|
1257
|
+
return [{ type: "error", message: `google response was not a JSON object (${valueType})` }];
|
|
1258
|
+
}
|
|
1259
|
+
raw = parsedRaw;
|
|
1162
1260
|
rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
|
|
1163
1261
|
const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
|
|
1164
1262
|
rawReservation.commitRetained();
|
|
@@ -1175,6 +1273,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
1175
1273
|
};
|
|
1176
1274
|
if (raw.error) {
|
|
1177
1275
|
const err = raw.error as { message?: string };
|
|
1276
|
+
handleSignatureRejection(err.message);
|
|
1178
1277
|
return finish([{ type: "error", message: err.message ?? "upstream error" }]);
|
|
1179
1278
|
}
|
|
1180
1279
|
// Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.
|
package/src/adapters/image.ts
CHANGED
|
@@ -18,6 +18,6 @@ export function parseDataUrl(url: string): { mediaType: string; base64: string }
|
|
|
18
18
|
*/
|
|
19
19
|
export function contentPartsToText(content: string | OcxContentPart[]): string {
|
|
20
20
|
if (typeof content === "string") return content;
|
|
21
|
-
const text = content.map(p =>
|
|
21
|
+
const text = content.map(p => p.type === "text" ? p.text : p.type === "image" ? "[image]" : "[video]").join("");
|
|
22
22
|
return text || "[image]";
|
|
23
23
|
}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
export const KIRO_COMPLETION_TOOL_NAME = "codex_kiro_final_answer";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request-scoped CodeWhisperer service profile for AWS Builder ID accounts.
|
|
5
|
+
*
|
|
6
|
+
* Builder ID is a personal identity with no AWS account behind it, so AWS never mints an
|
|
7
|
+
* account-scoped `profile/<id>` ARN for it. The Kiro CLI resolves this the same way: it carries
|
|
8
|
+
* this fixed service profile on Builder ID requests. The embedded account id is Amazon's own, not
|
|
9
|
+
* the user's, which is why sending it is not the same as synthesizing an account identity.
|
|
10
|
+
*
|
|
11
|
+
* Request-scoped is load-bearing. This value must never be persisted into `KiroOAuthMetadata`,
|
|
12
|
+
* never seed region inference (it is `us-east-1` and would pin every Builder ID account there),
|
|
13
|
+
* and never participate in account matching.
|
|
14
|
+
*/
|
|
15
|
+
export const KIRO_BUILDER_ID_SERVICE_PROFILE_ARN =
|
|
16
|
+
"arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX";
|
|
2
17
|
export const KIRO_CONTINUATION_MESSAGE =
|
|
3
18
|
"Continue from the prior conversation. Do not quote or mention this instruction.";
|
|
4
19
|
export const KIRO_COMPLETION_RETRY_MESSAGE =
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxParsedRequest, OcxTool } from "../types";
|
|
2
2
|
import { namespacedToolName } from "../types";
|
|
3
3
|
import { normalizeKiroModelId } from "../providers/kiro-models";
|
|
4
|
+
import { isCodexCodeModeExecTool } from "./tool-catalog-nudge";
|
|
4
5
|
import { createKiroToolNameRegistry, type KiroToolNameRegistry } from "./kiro-wire";
|
|
5
6
|
|
|
6
7
|
const MAX_KIRO_TOOL_DESCRIPTION_UNVERIFIED = 1024;
|
|
@@ -103,7 +104,7 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
|
|
|
103
104
|
// Seed with the root's own properties/required so a schema like
|
|
104
105
|
// { type:"object", properties:{path}, required:["path"], oneOf:[...] } keeps them.
|
|
105
106
|
if (obj.properties && typeof obj.properties === "object") {
|
|
106
|
-
Object.assign(props,
|
|
107
|
+
Object.assign(props, sanitizeSchemaMap(obj.properties) as Record<string, unknown>);
|
|
107
108
|
}
|
|
108
109
|
if (Array.isArray(obj.required)) {
|
|
109
110
|
for (const r of obj.required) if (typeof r === "string") required.add(r);
|
|
@@ -118,7 +119,7 @@ function ensureRootObjectType(schema: unknown): Record<string, unknown> {
|
|
|
118
119
|
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
119
120
|
const v = variant as Record<string, unknown>;
|
|
120
121
|
if (v.properties && typeof v.properties === "object") {
|
|
121
|
-
Object.assign(props,
|
|
122
|
+
Object.assign(props, sanitizeSchemaMap(v.properties) as Record<string, unknown>);
|
|
122
123
|
}
|
|
123
124
|
if (mergeRequired && Array.isArray(v.required)) {
|
|
124
125
|
for (const r of v.required) if (typeof r === "string") required.add(r);
|
|
@@ -174,8 +175,14 @@ function omittedToolCatalogNotice(kept: number, omitted: readonly OcxTool[], reg
|
|
|
174
175
|
|
|
175
176
|
function boundedCatalogPriority(tool: OcxTool): number {
|
|
176
177
|
if (tool.loadedFromToolSearch) return 0;
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// Codex code mode reaches shell, file edits, apply_patch and every MCP helper ONLY as nested
|
|
179
|
+
// `tools.<name>(...)` calls inside this one tool. Dropping it does not shrink the catalog, it
|
|
180
|
+
// makes the rest of the catalog uncallable -- so it outranks the search gateway and filler.
|
|
181
|
+
// It stays BEHIND `loadedFromToolSearch` because those are tools the model asked for by name
|
|
182
|
+
// this turn (#2475). Cursor pins its execution path the same way (request-builder.ts, #399).
|
|
183
|
+
if (isCodexCodeModeExecTool(tool)) return 1;
|
|
184
|
+
if (tool.toolSearch) return 2;
|
|
185
|
+
return 3;
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
export function convertKiroToolContext(
|
|
@@ -209,19 +216,40 @@ export function convertKiroToolContext(
|
|
|
209
216
|
const candidates = exceedsBudget
|
|
210
217
|
? convertedEntries.toSorted((a, b) => boundedCatalogPriority(a.tool) - boundedCatalogPriority(b.tool) || a.index - b.index)
|
|
211
218
|
: convertedEntries;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
// Reserve a seat for the code-mode execution path before filling the rest.
|
|
220
|
+
//
|
|
221
|
+
// Priority alone cannot save it. `loadedFromToolSearch` tools outrank it and arrive unbounded
|
|
222
|
+
// (the Responses parser pushes every `tool_search_output` spec), so a session that accumulated
|
|
223
|
+
// MAX_KIRO_TOOL_COUNT loaded tools would exhaust the budget before reaching tier 1 and drop the
|
|
224
|
+
// one tool through which all of them are actually callable.
|
|
225
|
+
//
|
|
226
|
+
// Reservation rather than eviction: this lowers the room the fill loop sees, so it admits one
|
|
227
|
+
// fewer tool. It never removes a tool that already fit, which is what keeps #2475's loaded-result
|
|
228
|
+
// guarantee intact -- Cursor's `evictNonExecutionPath` exempts only the execution path and could
|
|
229
|
+
// evict a loaded tool instead.
|
|
230
|
+
const reserved = candidates.find(entry => isCodexCodeModeExecTool(entry.tool));
|
|
231
|
+
const admitted = new Set<number>();
|
|
232
|
+
const filled: unknown[] = [];
|
|
233
|
+
for (const entry of candidates) {
|
|
234
|
+
if (entry === reserved) continue;
|
|
235
|
+
// Measure the projected FINAL array: the byte budget is computed over the serialized array, so
|
|
236
|
+
// subtracting a standalone size would misjudge it by the separators JSON adds between entries.
|
|
237
|
+
const projected = reserved ? [...filled, entry.converted, reserved.converted] : [...filled, entry.converted];
|
|
215
238
|
if (
|
|
216
|
-
|
|
217
|
-
|| serializedToolCatalogBytes(
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
convertedTools.push(entry.converted);
|
|
239
|
+
projected.length > MAX_KIRO_TOOL_COUNT
|
|
240
|
+
|| serializedToolCatalogBytes(projected) > MAX_KIRO_TOOL_CATALOG_BYTES
|
|
241
|
+
) break;
|
|
242
|
+
filled.push(entry.converted);
|
|
243
|
+
admitted.add(entry.index);
|
|
223
244
|
}
|
|
224
|
-
|
|
245
|
+
if (reserved) admitted.add(reserved.index);
|
|
246
|
+
// Rebuild in sorted-candidate order so the wire order stays loaded -> exec -> gateway -> filler.
|
|
247
|
+
// Pushing the reserved entry after the loop would place it last instead.
|
|
248
|
+
const convertedTools = candidates.filter(entry => admitted.has(entry.index)).map(entry => entry.converted);
|
|
249
|
+
// Derive omissions by set difference. The old `candidates.slice(omittedAt)` assumed every
|
|
250
|
+
// candidate after the first rejection was omitted, which stops being true once one of them was
|
|
251
|
+
// reserved and admitted: the notice would name `exec` unavailable while it is on the wire.
|
|
252
|
+
const omittedTools = candidates.filter(entry => !admitted.has(entry.index)).map(entry => entry.tool);
|
|
225
253
|
return {
|
|
226
254
|
tools: convertedTools,
|
|
227
255
|
systemAdditions: omittedTools.length > 0 ? [omittedToolCatalogNotice(convertedTools.length, omittedTools, registry)] : [],
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeEventStream } from "../lib/eventstream-decoder";
|
|
2
2
|
import { estimateTokens } from "../lib/token-estimate";
|
|
3
3
|
import { debugProviderDiagnostic } from "../lib/debug";
|
|
4
|
-
import { resolveKiroApiRegion,
|
|
4
|
+
import { resolveKiroApiRegion, resolveKiroRequestProfile } from "../oauth/kiro";
|
|
5
5
|
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
6
6
|
import { modelRecordValue } from "../reasoning-effort";
|
|
7
7
|
import { parseKiroEvent } from "./kiro-events";
|
|
@@ -33,6 +33,7 @@ import type {
|
|
|
33
33
|
OcxTextContent,
|
|
34
34
|
OcxToolCall,
|
|
35
35
|
OcxToolResultMessage,
|
|
36
|
+
OcxTool,
|
|
36
37
|
OcxUsage,
|
|
37
38
|
} from "../types";
|
|
38
39
|
import type { ProviderAdapter } from "./base";
|
|
@@ -42,7 +43,7 @@ import { sniffImageDimensions } from "./anthropic-image-guard";
|
|
|
42
43
|
import { fetchKiroWithRetry, noteKiroTransientThrottle } from "./kiro-retry";
|
|
43
44
|
import { convertKiroToolContext } from "./kiro-tools";
|
|
44
45
|
import { identifyRoutedModel } from "./identity";
|
|
45
|
-
import { buildNonOpenAIToolCatalogNudgeFromNames } from "./tool-catalog-nudge";
|
|
46
|
+
import { buildNonOpenAIToolCatalogNudgeFromNames, isBareShellBridgeTool, isCodexCodeModeExecTool } from "./tool-catalog-nudge";
|
|
46
47
|
import {
|
|
47
48
|
KIRO_COMPLETION_INSTRUCTIONS,
|
|
48
49
|
KIRO_COMPLETION_RETRY_MESSAGE,
|
|
@@ -321,9 +322,23 @@ function validateKiroCapabilities(parsed: OcxParsedRequest): void {
|
|
|
321
322
|
if (parsed.options.serviceTier !== undefined) {
|
|
322
323
|
throw new Error("Kiro does not support service tiers");
|
|
323
324
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
// Structured output is a real contract Kiro cannot honour: the wire has no
|
|
326
|
+
// schema-constrained response mode, so a caller expecting parseable JSON would receive
|
|
327
|
+
// prose and fail downstream. Refuse it.
|
|
328
|
+
//
|
|
329
|
+
// The rest of the Responses `text` object is not that. `text.verbosity` is a length
|
|
330
|
+
// preference and `text.format: {type:"text"}` is ordinary prose — the default output
|
|
331
|
+
// mode, which no capability flag governs and every correct client may send. Testing
|
|
332
|
+
// `_rawBody.text !== undefined` refused those turns for the mere PRESENCE of the key,
|
|
333
|
+
// the same mistake db040e70f removed one condition earlier where a permissive
|
|
334
|
+
// `parallel_tool_calls` hint was read as a requirement.
|
|
335
|
+
//
|
|
336
|
+
// Nothing needs stripping the way openai-responses strips a no-op verbosity:
|
|
337
|
+
// buildKiroPayload composes conversationState field by field from `parsed` and never
|
|
338
|
+
// spreads `_rawBody`, so a tolerated control is dropped by construction. The test
|
|
339
|
+
// asserts that absence so it stays true.
|
|
340
|
+
if (parsed._structuredOutput) {
|
|
341
|
+
throw new Error("Kiro does not support Responses structured output");
|
|
327
342
|
}
|
|
328
343
|
}
|
|
329
344
|
|
|
@@ -460,9 +475,32 @@ export function buildKiroPayload(
|
|
|
460
475
|
// name for a tool that was never advertised and pollute the collision domain.
|
|
461
476
|
const advertisedAlias = new Map<string, string>();
|
|
462
477
|
for (const [alias, wireName] of registry.nameMap) advertisedAlias.set(wireName, alias);
|
|
478
|
+
// Code mode is decided on the EMITTED catalog, not the requested list.
|
|
479
|
+
//
|
|
480
|
+
// `freeform` only exists on the requested tool objects -- `kiroToolWireNames` has already
|
|
481
|
+
// reduced the emitted catalog to strings -- so the predicates must read the objects. But the
|
|
482
|
+
// SHAPE that matters is the one the model receives: the count/byte budget can drop a requested
|
|
483
|
+
// `exec_command` while `exec` survives, and scanning the requested list would then find a shell
|
|
484
|
+
// bridge the model cannot call and suppress code mode for a catalog that is code-mode-shaped.
|
|
485
|
+
// Intersecting the two keeps `tool_choice: "none"` and budget omission correct for free: both
|
|
486
|
+
// empty the emitted set, so nothing can be named.
|
|
487
|
+
const emittedToolNames = new Set(kiroToolWireNames(kiroTools));
|
|
488
|
+
const emittedAlias = (tool: OcxTool): string | undefined => {
|
|
489
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
490
|
+
// Read the recorded mapping; `registry.alias()` would REGISTER a name here.
|
|
491
|
+
const alias = advertisedAlias.get(wireName) ?? wireName;
|
|
492
|
+
return emittedToolNames.has(alias) ? alias : undefined;
|
|
493
|
+
};
|
|
494
|
+
const requestedTools = parsed.context.tools ?? [];
|
|
495
|
+
const emittedCodeModeExec = requestedTools.find(tool => isCodexCodeModeExecTool(tool) && emittedAlias(tool));
|
|
496
|
+
const emittedShellBridge = requestedTools.some(tool => isBareShellBridgeTool(tool) && emittedAlias(tool));
|
|
497
|
+
const codeModeExecName = emittedCodeModeExec && !emittedShellBridge
|
|
498
|
+
? emittedAlias(emittedCodeModeExec)
|
|
499
|
+
: undefined;
|
|
463
500
|
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeFromNames(
|
|
464
501
|
kiroToolWireNames(kiroTools),
|
|
465
502
|
name => advertisedAlias.get(name) ?? name,
|
|
503
|
+
codeModeExecName,
|
|
466
504
|
);
|
|
467
505
|
const boundedNudge = toolCatalogNudge ? boundedInjectedInstruction(toolCatalogNudge, injectedChars) : undefined;
|
|
468
506
|
if (boundedNudge) systemParts.push(boundedNudge);
|
|
@@ -1723,12 +1761,19 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1723
1761
|
throw new Error("kiro token missing — run ocx login kiro");
|
|
1724
1762
|
}
|
|
1725
1763
|
const region = resolveKiroApiRegion(parsed._kiroAuthContext);
|
|
1726
|
-
|
|
1764
|
+
// Request-scoped: an AWS Builder ID account has no profile of its own and resolves to Kiro's
|
|
1765
|
+
// fixed service profile here, without that value ever becoming the account's stored identity.
|
|
1766
|
+
const requestProfile = resolveKiroRequestProfile(parsed._kiroAuthContext);
|
|
1767
|
+
const resolvedProfileArn = requestProfile.profileArn;
|
|
1727
1768
|
const isApiKey = provider.apiKey.trim().startsWith("ksk_");
|
|
1728
1769
|
const profileArn = isApiKey ? undefined : resolvedProfileArn;
|
|
1729
|
-
// Builder ID and Kiro API keys
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1770
|
+
// Builder ID and Kiro API keys are accepted only on Kiro's CLI request path; enterprise
|
|
1771
|
+
// profiles retain the IDE-shaped request. Builder ID now carries a profile ARN, so a truthy
|
|
1772
|
+
// `profileArn` no longer implies "enterprise". The wire path reads the resolver's own verdict
|
|
1773
|
+
// rather than re-deriving it, so the accountless path — where the auth type comes from the
|
|
1774
|
+
// local import, not the request context — cannot send the fallback inside an IDE-shaped call.
|
|
1775
|
+
const isBuilderId = requestProfile.builderIdFallback;
|
|
1776
|
+
const wireClient: KiroWireClient = isApiKey || isBuilderId || !profileArn ? "cli" : "ide";
|
|
1732
1777
|
const fp = fingerprint().slice(0, 64);
|
|
1733
1778
|
const headers: Record<string, string> = wireClient === "cli" ? {
|
|
1734
1779
|
authorization: `Bearer ${provider.apiKey}`,
|