@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { OcxTool } from "../../types";
|
|
2
|
+
import { CODEX_SHELL_COMMAND_TOOL, isBareCodexExecCommandTool, isBareCodexShellBridgeTool, isCodexShellBridgeToolName } from "./tool-naming";
|
|
3
|
+
|
|
4
|
+
export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
cmd: { type: "string", description: "Shell command to execute." },
|
|
8
|
+
workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." },
|
|
9
|
+
shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." },
|
|
10
|
+
tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
|
|
11
|
+
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
12
|
+
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
13
|
+
},
|
|
14
|
+
required: ["cmd"],
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Structured single-replacement schema advertised to Cursor models in addition to the freeform
|
|
20
|
+
* `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native
|
|
21
|
+
* Edit shape) but cannot produce Codex's freeform patch grammar, so every file edit attempt on the
|
|
22
|
+
* Cursor route produced malformed `apply_patch` payloads that the Codex client rejected locally
|
|
23
|
+
* (#1017). Calls to this tool are converted server-side into a valid apply_patch payload.
|
|
24
|
+
*/
|
|
25
|
+
export const CURSOR_EDIT_FILE_INPUT_SCHEMA = {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
|
|
29
|
+
old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
|
|
30
|
+
new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
|
|
31
|
+
},
|
|
32
|
+
required: ["file_path", "old_string", "new_string"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/** Structured multi-replacement schema; mirrors Cursor's native MultiEdit shape. */
|
|
37
|
+
export const CURSOR_MULTI_EDIT_INPUT_SCHEMA = {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
file_path: { type: "string", description: "Path of the file to edit, relative to the workspace root." },
|
|
41
|
+
edits: {
|
|
42
|
+
type: "array",
|
|
43
|
+
items: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
old_string: { type: "string", description: "Exact text to replace. Must match the current file content, including line breaks." },
|
|
47
|
+
new_string: { type: "string", description: "Replacement text. Empty removes the matched text." },
|
|
48
|
+
},
|
|
49
|
+
required: ["old_string", "new_string"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
description: "Ordered replacement edits for this file. Each old_string must match the current file content.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["file_path", "edits"],
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Responses/Codex-side schema used ONLY for arg-key normalization after Cursor returns a call.
|
|
61
|
+
* Cursor models are trained to emit `cmd`; Codex `shell_command` / `exec_command` validate
|
|
62
|
+
* `command`. Keeping `cmd` out of this schema lets `normalizeArgKeys` rewrite `cmd` → `command`.
|
|
63
|
+
*/
|
|
64
|
+
export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
command: { type: "string", description: "Shell command to execute." },
|
|
68
|
+
workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." },
|
|
69
|
+
shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." },
|
|
70
|
+
tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
|
|
71
|
+
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
72
|
+
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
73
|
+
max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
|
|
74
|
+
},
|
|
75
|
+
required: ["command"],
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
|
|
80
|
+
export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
81
|
+
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Schema used to normalize completed Cursor tool args back to Responses/Codex field names.
|
|
86
|
+
* Must NOT reuse `cursorToolInputSchema` for the shell bridge: advertising `cmd` while also
|
|
87
|
+
* treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
|
|
88
|
+
*/
|
|
89
|
+
export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
|
|
90
|
+
if (isBareCodexShellBridgeTool(tool)) {
|
|
91
|
+
return shellBridgeArgNormalizeSchema(tool);
|
|
92
|
+
}
|
|
93
|
+
return tool.parameters ?? {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shellBridgeArgNormalizeSchema(tool: OcxTool): unknown {
|
|
97
|
+
const parameters = tool.parameters;
|
|
98
|
+
if (!parameters || typeof parameters !== "object") return CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA;
|
|
99
|
+
const base = parameters as Record<string, unknown>;
|
|
100
|
+
const rawProps = base.properties && typeof base.properties === "object"
|
|
101
|
+
? { ...(base.properties as Record<string, unknown>) }
|
|
102
|
+
: {};
|
|
103
|
+
const required = Array.isArray(base.required) ? [...base.required as unknown[]] : [];
|
|
104
|
+
const requiresCommand = required.includes("command") || "command" in rawProps;
|
|
105
|
+
const requiresCmd = required.includes("cmd") || "cmd" in rawProps;
|
|
106
|
+
const shouldRewriteCmdToCommand = tool.name === CODEX_SHELL_COMMAND_TOOL || requiresCommand;
|
|
107
|
+
|
|
108
|
+
if (!shouldRewriteCmdToCommand && requiresCmd) {
|
|
109
|
+
return parameters;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Drop Cursor-preferred aliases so normalizeArgKeys can rewrite them to Responses keys.
|
|
113
|
+
delete rawProps.cmd;
|
|
114
|
+
const properties = {
|
|
115
|
+
...CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties,
|
|
116
|
+
...rawProps,
|
|
117
|
+
command: rawProps.command ?? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA.properties.command,
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
type: "object",
|
|
122
|
+
properties,
|
|
123
|
+
required: requiresCommand ? required : ["command"],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Required command payload keys for a shell bridge tool, derived from the advertised schema when present.
|
|
129
|
+
*/
|
|
130
|
+
export function shellBridgeRequiredCommandKeys(
|
|
131
|
+
toolName: string,
|
|
132
|
+
schema?: unknown,
|
|
133
|
+
): readonly ("cmd" | "command")[] {
|
|
134
|
+
if (schema && typeof schema === "object") {
|
|
135
|
+
const required = (schema as Record<string, unknown>).required;
|
|
136
|
+
if (Array.isArray(required)) {
|
|
137
|
+
const keys = required.filter((key): key is "cmd" | "command" => key === "cmd" || key === "command");
|
|
138
|
+
if (keys.length > 0) return keys;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return toolName === CODEX_SHELL_COMMAND_TOOL ? ["command"] : ["cmd"];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Normalize-schema defaults used when validating stateless synthetic shell-bridge calls. */
|
|
145
|
+
export function defaultShellBridgeArgNormalizeSchema(toolName: string): unknown {
|
|
146
|
+
return toolName === CODEX_SHELL_COMMAND_TOOL
|
|
147
|
+
? CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA
|
|
148
|
+
: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: CURSOR_EXEC_COMMAND_INPUT_SCHEMA.properties,
|
|
151
|
+
required: ["cmd"],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function cursorShellBridgeDropError(toolName: string): string {
|
|
156
|
+
return `Cursor emitted ${toolName} without a non-empty command; the tool call was dropped.`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extract a non-empty shell command from completed Cursor bridge args using the schema's required
|
|
161
|
+
* command key (`cmd` for bare exec_command, `command` for shell_command).
|
|
162
|
+
*/
|
|
163
|
+
export function nonEmptyShellBridgeCommandFromArgs(
|
|
164
|
+
finalArgs: string,
|
|
165
|
+
toolName: string,
|
|
166
|
+
schema?: unknown,
|
|
167
|
+
): string | undefined {
|
|
168
|
+
let parsed: unknown;
|
|
169
|
+
try {
|
|
170
|
+
parsed = finalArgs.length > 0 ? JSON.parse(finalArgs) : {};
|
|
171
|
+
} catch {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
175
|
+
const record = parsed as Record<string, unknown>;
|
|
176
|
+
const requiredKeys = shellBridgeRequiredCommandKeys(toolName, schema);
|
|
177
|
+
const candidateKeys = new Set<"cmd" | "command">([
|
|
178
|
+
...requiredKeys,
|
|
179
|
+
requiredKeys.includes("cmd") ? "command" : "cmd",
|
|
180
|
+
]);
|
|
181
|
+
for (const key of candidateKeys) {
|
|
182
|
+
const value = record[key];
|
|
183
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function cursorShellBridgeArgsValid(
|
|
189
|
+
finalArgs: string,
|
|
190
|
+
toolName: string,
|
|
191
|
+
schema?: unknown,
|
|
192
|
+
): boolean {
|
|
193
|
+
return !isCodexShellBridgeToolName(toolName)
|
|
194
|
+
|| nonEmptyShellBridgeCommandFromArgs(finalArgs, toolName, schema) !== undefined;
|
|
195
|
+
}
|
|
@@ -31,7 +31,7 @@ export interface CursorRunRequest {
|
|
|
31
31
|
/**
|
|
32
32
|
* Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
|
|
33
33
|
* When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
|
|
34
|
-
* the userMessageAction
|
|
34
|
+
* the userMessageAction prefix instead of the standard continuation text; rawMessages stay
|
|
35
35
|
* untouched so history replay is unchanged.
|
|
36
36
|
*/
|
|
37
37
|
echoRetryContinuationText?: string;
|
|
@@ -40,9 +40,12 @@ export interface CursorRunRequest {
|
|
|
40
40
|
messages: CursorRequestMessage[];
|
|
41
41
|
rawMessages?: readonly OcxMessage[];
|
|
42
42
|
/**
|
|
43
|
-
* Images for the active user/developer turn
|
|
43
|
+
* Images for the active user/developer turn or an external model's trailing tool-result run.
|
|
44
|
+
* Encoded as SelectedImage blobIdWithData refs under
|
|
44
45
|
* UserMessage.selected_context (bytes live in the request-scoped KV store for getBlobArgs
|
|
45
|
-
* hydration).
|
|
46
|
+
* hydration). Bounded sourceLabel metadata from tool images is appended to the active action,
|
|
47
|
+
* outside prunable history; it is not a new image wire field. Native Composer selection stays
|
|
48
|
+
* unchanged. History stays text-only. data: URLs only in this slice.
|
|
46
49
|
*/
|
|
47
50
|
selectedImages?: readonly ResolvedCursorImage[];
|
|
48
51
|
tools?: OcxTool[];
|
|
@@ -113,7 +113,7 @@ export const EMPTY_EXEC_OUTPUT_MESSAGE =
|
|
|
113
113
|
* that drifts apart is how a model gets told two different things about the same isolate.
|
|
114
114
|
*/
|
|
115
115
|
export const CODE_MODE_RESULT_ECHO_SENTENCE =
|
|
116
|
-
"Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd:
|
|
116
|
+
"Nothing in the isolate is echoed automatically: a bare trailing `await tools.<name>(...)` or final expression value is DISCARDED, and the cell reports empty output. Pass anything you need to read to `text(...)` (or `notify(...)`) in the same cell — for example `text(JSON.stringify(await tools.exec_command({cmd: 'ls'})))` — and treat an empty result as your own missing `text(...)` call rather than a failed command or lost context.";
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
119
|
* Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error";
|
|
2
|
+
import { isLocationUnsupportedMessage } from "../lib/errors";
|
|
2
3
|
|
|
3
4
|
/** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
|
|
4
5
|
function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
|
|
@@ -65,9 +66,16 @@ function classifyGoogle(label: string, status: number | undefined, enumStatus: s
|
|
|
65
66
|
if (status === 401 || enumStatus === "UNAUTHENTICATED" || lower.includes("unauthenticated") || lower.includes("invalid authentication") || lower.includes("expired")) {
|
|
66
67
|
return `${label} authentication failed`;
|
|
67
68
|
}
|
|
68
|
-
if (status === 403 || enumStatus === "PERMISSION_DENIED" || lower.includes("permission denied") || lower.includes("access denied")) {
|
|
69
|
+
if (status === 403 || enumStatus === "PERMISSION_DENIED" || lower.includes("permission_denied") || lower.includes("permission denied") || lower.includes("access denied")) {
|
|
69
70
|
return `${label} access denied`;
|
|
70
71
|
}
|
|
72
|
+
// Google rejects unsupported geographic / datacenter locations with HTTP 400
|
|
73
|
+
// FAILED_PRECONDITION. The payload is not malformed, so it must not fall through to
|
|
74
|
+
// "invalid request" (#3467). Only the observed 400/precondition envelope permits
|
|
75
|
+
// this inference; other explicit enums and server statuses remain authoritative.
|
|
76
|
+
if (status === 400 && (!enumStatus || enumStatus === "FAILED_PRECONDITION") && isLocationUnsupportedMessage(lower)) {
|
|
77
|
+
return `${label} location not supported`;
|
|
78
|
+
}
|
|
71
79
|
if (status === 503 || enumStatus === "UNAVAILABLE" || lower.includes("overloaded") || lower.includes("unavailable")) {
|
|
72
80
|
return `${label} server overloaded`;
|
|
73
81
|
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -52,6 +52,7 @@ const GOOGLE_BREVITY_INSTRUCTION = [
|
|
|
52
52
|
"- Do detailed reasoning internally, not as visible intermediate output.",
|
|
53
53
|
"- Prefer taking the next tool action over explaining; keep calling tools until the task is complete.",
|
|
54
54
|
"- 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.",
|
|
55
|
+
"- Formatting: The client environment renders standard Markdown and does not support LaTeX math delimiters ($...$, $$...$$, \\(...\\), \\[...\\]). Do not use LaTeX math delimiters or LaTeX markup (such as \\text{}, \\times, \\le, \\ge, etc.) for variables, formulas, dimensions, or units. Use clean plain text, Markdown, and Unicode symbols (e.g. 180°, 2560 × 1920 px, ≤, ≥, Δ, ±) instead.",
|
|
55
56
|
].join("\n");
|
|
56
57
|
|
|
57
58
|
const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-conversation calibration of the Kiro input estimate.
|
|
3
|
+
*
|
|
4
|
+
* The estimator in `lib/token-estimate` is a fixed heuristic: constants derived from recorded
|
|
5
|
+
* traffic, applied to every account and every conversation identically. It is close on average
|
|
6
|
+
* and necessarily wrong in the particular, because how densely a prompt tokenizes depends on
|
|
7
|
+
* what is in it — a Korean design discussion and a repository of minified JSON do not share a
|
|
8
|
+
* ratio, and no constant can serve both.
|
|
9
|
+
*
|
|
10
|
+
* Kiro, however, tells us the answer. Mid-stream it reports `contextUsagePercentage`, which
|
|
11
|
+
* against a known window is an authoritative token count for the exact payload just sent. Today
|
|
12
|
+
* that number is used once, as a floor under the current turn's total, and then discarded. The
|
|
13
|
+
* conversation's own measured tokens-per-character is therefore recomputed from scratch, and
|
|
14
|
+
* mispredicted the same way, on every single turn.
|
|
15
|
+
*
|
|
16
|
+
* This module keeps it. After a TERMINAL attempt that produced both an estimate and a reported
|
|
17
|
+
* percentage, the realised ratio `charged / estimated` is folded into a per-conversation
|
|
18
|
+
* correction and applied to the next turn of that conversation, so a long conversation converges
|
|
19
|
+
* on the rate it actually exhibits rather than the rate the average conversation exhibits.
|
|
20
|
+
*
|
|
21
|
+
* "Terminal" matters: the adapter's bounded completion retry streams a second time for the same
|
|
22
|
+
* user turn against a rebuilt payload, so the caller commits an observation only once that turn
|
|
23
|
+
* is genuinely over.
|
|
24
|
+
*
|
|
25
|
+
* Four properties keep this from being able to make the gauge worse:
|
|
26
|
+
*
|
|
27
|
+
* - **Bounded.** The factor is clamped, so a single anomalous reading cannot distort the
|
|
28
|
+
* estimate by an unbounded amount, and a malformed or hostile percentage cannot either.
|
|
29
|
+
* - **Smoothed.** Each observation moves the factor part of the way rather than replacing it, so
|
|
30
|
+
* one cache-affected or truncated turn cannot swing the gauge.
|
|
31
|
+
* - **Bounded in memory.** Conversation-scoped with an eviction cap and no persistence. This is
|
|
32
|
+
* a hint that improves with use, not state anything depends on.
|
|
33
|
+
* - **Subordinate to the truth.** The existing upstream floor is untouched. Calibration only
|
|
34
|
+
* sharpens the estimate BEFORE upstream reports; it can never lower a value upstream has
|
|
35
|
+
* already justified.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately not in `lib/token-estimate`: that module is pure, shared by every provider, and
|
|
38
|
+
* must stay so. This is Kiro-specific state and lives with the Kiro adapter.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Clamp for the correction factor.
|
|
43
|
+
*
|
|
44
|
+
* Wide enough to absorb the genuine spread between conversation kinds, narrow enough that a
|
|
45
|
+
* wrong reading cannot produce a nonsensical gauge. A factor below 1 shrinks the estimate, so
|
|
46
|
+
* the lower bound is the more dangerous side and is kept nearer 1.
|
|
47
|
+
*/
|
|
48
|
+
const MIN_FACTOR = 0.7;
|
|
49
|
+
const MAX_FACTOR = 3;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Weight given to a new observation. 0.35 reaches most of the way to a persistent new rate
|
|
53
|
+
* within a few turns while still requiring more than one reading to move far.
|
|
54
|
+
*/
|
|
55
|
+
const SMOOTHING = 0.35;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Maximum conversations tracked. Entries are small, but the map must not grow with uptime.
|
|
59
|
+
*
|
|
60
|
+
* Eviction is least-recently-USED, not oldest-inserted: `touch` re-inserts on every estimate and
|
|
61
|
+
* every observation, so an active long conversation survives an arbitrary number of short ones
|
|
62
|
+
* started after it. Insertion order would have evicted exactly the conversation most worth
|
|
63
|
+
* keeping.
|
|
64
|
+
*/
|
|
65
|
+
const MAX_TRACKED_CONVERSATIONS = 256;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A ratio this far from 1 is not a mis-calibrated estimate, it is a different measurement:
|
|
69
|
+
* a compaction, a cache boundary, or a percentage reported against a window we did not expect.
|
|
70
|
+
* Learning from it would teach the wrong lesson, so it is ignored entirely.
|
|
71
|
+
*/
|
|
72
|
+
const MAX_PLAUSIBLE_OBSERVATION = 6;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One conversation's calibration state.
|
|
76
|
+
*
|
|
77
|
+
* `factor` is the learned correction. `rawEstimate` is the uncorrected estimate most recently
|
|
78
|
+
* applied: the stream records what upstream charged, but by then the value it holds has already
|
|
79
|
+
* been corrected, and learning from that would make the factor measure its own residual error
|
|
80
|
+
* instead of the heuristic's — each round would close only the part it had not yet fixed and the
|
|
81
|
+
* factor would converge short of the true ratio.
|
|
82
|
+
*
|
|
83
|
+
* Both live in ONE entry so they cannot desynchronise. Held as two maps, a conversation could be
|
|
84
|
+
* refreshed in one and evicted from the other, and the pair could retain twice the documented
|
|
85
|
+
* bound in distinct ids.
|
|
86
|
+
*/
|
|
87
|
+
interface Calibration {
|
|
88
|
+
factor?: number;
|
|
89
|
+
rawEstimate?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Insertion-ordered, so the first key is the least recently touched entry. */
|
|
93
|
+
const calibrations = new Map<string, Calibration>();
|
|
94
|
+
|
|
95
|
+
/** Re-insert to move an entry to the most-recently-used end, then evict from the old end. */
|
|
96
|
+
function touch(conversationId: string, update: (current: Calibration) => Calibration): void {
|
|
97
|
+
const current = calibrations.get(conversationId) ?? {};
|
|
98
|
+
calibrations.delete(conversationId);
|
|
99
|
+
calibrations.set(conversationId, update(current));
|
|
100
|
+
while (calibrations.size > MAX_TRACKED_CONVERSATIONS) {
|
|
101
|
+
const oldest = calibrations.keys().next();
|
|
102
|
+
if (oldest.done) break;
|
|
103
|
+
calibrations.delete(oldest.value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function clamp(value: number): number {
|
|
108
|
+
return Math.min(MAX_FACTOR, Math.max(MIN_FACTOR, value));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Record what a conversation was actually charged against what we estimated.
|
|
113
|
+
*
|
|
114
|
+
* `estimated` must be the RAW estimate — what the fixed heuristic produced before any correction
|
|
115
|
+
* was applied — and `charged` the token count implied by the upstream percentage. Passing the
|
|
116
|
+
* already-corrected value instead would make the factor measure its own residual error rather
|
|
117
|
+
* than the heuristic's, so each round would learn only the part it had not yet fixed and the
|
|
118
|
+
* factor would settle short of the true ratio, permanently under-correcting.
|
|
119
|
+
*
|
|
120
|
+
* Non-finite, non-positive, or implausible inputs are ignored rather than clamped, because a bad
|
|
121
|
+
* reading carries no information worth smoothing in.
|
|
122
|
+
*/
|
|
123
|
+
export function recordKiroCalibration(
|
|
124
|
+
conversationId: string | undefined,
|
|
125
|
+
estimated: number,
|
|
126
|
+
charged: number,
|
|
127
|
+
): void {
|
|
128
|
+
if (!conversationId) return;
|
|
129
|
+
if (!Number.isFinite(estimated) || !Number.isFinite(charged)) return;
|
|
130
|
+
if (estimated <= 0 || charged <= 0) return;
|
|
131
|
+
const entry = calibrations.get(conversationId);
|
|
132
|
+
// Prefer the raw estimate recorded at build time; fall back to the caller's value when this
|
|
133
|
+
// conversation was never seen by `calibrateKiroEstimate` (a first turn, or a rebuilt retry).
|
|
134
|
+
const baseline = entry?.rawEstimate ?? estimated;
|
|
135
|
+
if (!Number.isFinite(baseline) || baseline <= 0) return;
|
|
136
|
+
const observed = charged / baseline;
|
|
137
|
+
if (!Number.isFinite(observed) || observed <= 0 || observed > MAX_PLAUSIBLE_OBSERVATION) return;
|
|
138
|
+
|
|
139
|
+
// Smooth from 1 (the "no correction" prior) on the FIRST observation too. Jumping straight to
|
|
140
|
+
// `clamp(observed)` would let a single cache-affected or truncated turn set the factor outright,
|
|
141
|
+
// which is exactly the swing the smoothing exists to prevent.
|
|
142
|
+
const previous = entry?.factor ?? 1;
|
|
143
|
+
const next = clamp(previous + (observed - previous) * SMOOTHING);
|
|
144
|
+
|
|
145
|
+
// The observation for this turn is consumed: dropping the raw estimate keeps a later record for
|
|
146
|
+
// the same conversation from being scored a second time against a stale baseline, which would
|
|
147
|
+
// re-learn the same correction from a payload that has since grown.
|
|
148
|
+
touch(conversationId, () => ({ factor: next }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Apply a conversation's learned correction to an estimate. Unknown conversations are returned
|
|
153
|
+
* unchanged, so the first turn behaves exactly as it did before calibration existed.
|
|
154
|
+
*/
|
|
155
|
+
export function calibrateKiroEstimate(conversationId: string | undefined, estimate: number): number {
|
|
156
|
+
if (!conversationId || !Number.isFinite(estimate) || estimate <= 0) return estimate;
|
|
157
|
+
touch(conversationId, current => ({ ...current, rawEstimate: estimate }));
|
|
158
|
+
const factor = calibrations.get(conversationId)?.factor;
|
|
159
|
+
return factor === undefined ? estimate : Math.ceil(estimate * factor);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Carry a conversation's state to the id upstream actually returned.
|
|
164
|
+
*
|
|
165
|
+
* The estimate is stored under the id the request was BUILT with, but Kiro can answer with a
|
|
166
|
+
* different conversation id, and the stream then records under that one. Without this the record
|
|
167
|
+
* would miss its own raw estimate and silently fall back to the corrected value — the exact
|
|
168
|
+
* feedback bug the raw estimate exists to avoid.
|
|
169
|
+
*/
|
|
170
|
+
export function rekeyKiroCalibration(fromConversationId: string | undefined, toConversationId: string | undefined): void {
|
|
171
|
+
if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return;
|
|
172
|
+
const entry = calibrations.get(fromConversationId);
|
|
173
|
+
if (!entry) return;
|
|
174
|
+
calibrations.delete(fromConversationId);
|
|
175
|
+
touch(toConversationId, current => ({ ...current, ...entry }));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Test seam: drop all learned state. */
|
|
179
|
+
export function resetKiroCalibration(): void {
|
|
180
|
+
calibrations.clear();
|
|
181
|
+
}
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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";
|
|
8
|
+
import { calibrateKiroEstimate, recordKiroCalibration, rekeyKiroCalibration } from "./kiro-calibration";
|
|
8
9
|
import {
|
|
9
10
|
classifyKiroEventError,
|
|
10
11
|
classifyKiroHttpError,
|
|
@@ -186,6 +187,85 @@ function estimateKiroTokens(text: string, modelId?: string): number {
|
|
|
186
187
|
return estimateTokens(text, modelId ? `kiro/${modelId}` : "kiro");
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/** Hangul/Han/kana ranges, matching the shared estimator's own CJK classification. */
|
|
191
|
+
function kiroCjkCount(text: string): number {
|
|
192
|
+
let cjk = 0;
|
|
193
|
+
for (let i = 0; i < text.length; i++) {
|
|
194
|
+
const c = text.charCodeAt(i);
|
|
195
|
+
if (
|
|
196
|
+
(c >= 0xac00 && c <= 0xd7a3) || (c >= 0x1100 && c <= 0x11ff) || (c >= 0x3130 && c <= 0x318f)
|
|
197
|
+
|| (c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x3040 && c <= 0x30ff)
|
|
198
|
+
) cjk++;
|
|
199
|
+
}
|
|
200
|
+
return cjk;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Token estimate for walked payload text, with the wire expansion applied to the Latin portion
|
|
205
|
+
* only. Splitting here rather than inside the shared estimator keeps that module pure and
|
|
206
|
+
* provider-neutral: the expansion is a fact about Kiro's wire, not about tokenization.
|
|
207
|
+
*/
|
|
208
|
+
function estimateKiroWireTokens(text: string, modelId: string): number {
|
|
209
|
+
if (!text) return 0;
|
|
210
|
+
const cjk = kiroCjkCount(text);
|
|
211
|
+
if (cjk === 0) return Math.ceil(estimateKiroTokens(text, modelId) * KIRO_LATIN_WIRE_EXPANSION);
|
|
212
|
+
const latinTokens = estimateKiroTokens("x".repeat(text.length - cjk), modelId);
|
|
213
|
+
const cjkTokens = estimateKiroTokens("\uac00".repeat(cjk), modelId);
|
|
214
|
+
return Math.ceil(latinTokens * KIRO_LATIN_WIRE_EXPANSION + cjkTokens);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Structural cost of one conversation entry, in tokens.
|
|
219
|
+
*
|
|
220
|
+
* The walker below concatenates message TEXT, but the wire carries JSON: per-entry keys
|
|
221
|
+
* (`userInputMessage`, `content`, `modelId`, `origin`) and role framing. That is charged
|
|
222
|
+
* upstream and is invisible to a text-only count, so without it a long conversation drifts
|
|
223
|
+
* further below the real charge with every turn added — an error proportional to entry COUNT,
|
|
224
|
+
* which no per-character ratio can recover.
|
|
225
|
+
*
|
|
226
|
+
* Regressing serialized bodies against what the walker counts, over eleven payload sizes from
|
|
227
|
+
* 3 to 701 entries:
|
|
228
|
+
*
|
|
229
|
+
* bodyBytes = 1.0422 * walkedChars + 66.7 * entries + 68
|
|
230
|
+
*
|
|
231
|
+
* 66.7 bytes at the measured 2.433 bytes per charged token is 27.4 tokens per entry. The
|
|
232
|
+
* earlier value of 12 was a conservative hand-fit taken before that regression existed, and
|
|
233
|
+
* being less than half the real cost is precisely why the estimate decayed with conversation
|
|
234
|
+
* length: an under-charge of ~15 tokens per entry is invisible across four messages and
|
|
235
|
+
* dominant across seven hundred.
|
|
236
|
+
*
|
|
237
|
+
* Cross-checked against 4,090 recorded requests, where real traffic averages 1,310 bytes per
|
|
238
|
+
* message: 66.7 bytes is 5% of that, so this term charges framing and is not quietly absorbing
|
|
239
|
+
* message content.
|
|
240
|
+
*/
|
|
241
|
+
const KIRO_ENTRY_FRAMING_TOKENS = 27;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Multiplier reconciling the LATIN text estimate with what the wire charges for that same text.
|
|
245
|
+
*
|
|
246
|
+
* The shared estimator counts Latin text at 2.8 chars/token, while the wire charges 2.433 bytes
|
|
247
|
+
* per token at 1.0422 bytes per walked character — an effective 2.334 chars/token, and
|
|
248
|
+
* 2.8 / 2.334 = 1.199.
|
|
249
|
+
*
|
|
250
|
+
* The evidence that the split between this term and `KIRO_ENTRY_FRAMING_TOKENS` is right is its
|
|
251
|
+
* stability: holding framing at 27, the multiplier the charge implies stays within 1.189-1.209
|
|
252
|
+
* across a 230x range of conversation sizes. A mis-specified split drifts with size, and the
|
|
253
|
+
* earlier 1.12/12 pair did — its accuracy fell from 0.92 at four messages to 0.87 at seven
|
|
254
|
+
* hundred.
|
|
255
|
+
*
|
|
256
|
+
* LATIN ONLY, deliberately. 2.433 bytes/token is a property of this traffic mix, which is Latin
|
|
257
|
+
* and code. A Hangul character is three UTF-8 bytes but roughly one token, so its bytes-per-token
|
|
258
|
+
* is entirely different and a Latin-derived byte rate says nothing about it. Scaling CJK by this
|
|
259
|
+
* factor bills Hangul at 1.25 chars/token, against recorded ground truth that already places the
|
|
260
|
+
* shared 1.5 ratio at 0.90 of the authoritative count — an over-charge that would compact Korean
|
|
261
|
+
* threads early.
|
|
262
|
+
*
|
|
263
|
+
* This is NOT JSON escaping, despite what an earlier version of this comment claimed. Measured
|
|
264
|
+
* directly, `JSON.stringify` expands prose by 1.012 (Latin) to 1.019 (Korean), nowhere near 1.2.
|
|
265
|
+
* Escaping is real but small, and is already inside the byte measurement this factor comes from.
|
|
266
|
+
*/
|
|
267
|
+
const KIRO_LATIN_WIRE_EXPANSION = 1.2;
|
|
268
|
+
|
|
189
269
|
function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelId: string): number {
|
|
190
270
|
const conversationState = (payload as {
|
|
191
271
|
conversationState?: {
|
|
@@ -216,7 +296,9 @@ function estimateKiroPayloadInputTokens(payload: Record<string, unknown>, modelI
|
|
|
216
296
|
if (assistant.toolUses?.length) parts.push(serializeForUsage(assistant.toolUses));
|
|
217
297
|
}
|
|
218
298
|
}
|
|
219
|
-
return
|
|
299
|
+
return estimateKiroWireTokens(parts.join("\n"), modelId)
|
|
300
|
+
+ imageTokens
|
|
301
|
+
+ entries.length * KIRO_ENTRY_FRAMING_TOKENS;
|
|
220
302
|
}
|
|
221
303
|
|
|
222
304
|
function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
|
|
@@ -990,6 +1072,11 @@ async function* parseKiroAttempt(
|
|
|
990
1072
|
// the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
|
|
991
1073
|
const deferred: AdapterEvent[] = [];
|
|
992
1074
|
const retention = createKiroAttemptRetention(budget);
|
|
1075
|
+
// Shared box: the inner parser stages its calibration observation here on the completion path,
|
|
1076
|
+
// and this wrapper decides whether the attempt was terminal enough to commit it. A box rather
|
|
1077
|
+
// than a return field because the completion path has a dozen terminal returns and threading a
|
|
1078
|
+
// field through every one of them is exactly the kind of edit that misses one.
|
|
1079
|
+
const attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } } = {};
|
|
993
1080
|
const attempt = parseKiroAttemptEvents(
|
|
994
1081
|
response,
|
|
995
1082
|
budget,
|
|
@@ -1001,12 +1088,22 @@ async function* parseKiroAttempt(
|
|
|
1001
1088
|
conversationId,
|
|
1002
1089
|
deferred,
|
|
1003
1090
|
retention,
|
|
1091
|
+
attemptCalibration,
|
|
1004
1092
|
contextInputEstimate,
|
|
1005
1093
|
priorEmittedOutput,
|
|
1006
1094
|
);
|
|
1007
1095
|
let handedOff = false;
|
|
1008
1096
|
try {
|
|
1009
1097
|
const result = yield* attempt;
|
|
1098
|
+
// A staged observation only counts when this attempt is the LAST one for the user turn. An
|
|
1099
|
+
// attempt that asks for the bounded fallback streams again against a rebuilt payload, so
|
|
1100
|
+
// committing here would move the factor twice for one turn and score the second observation
|
|
1101
|
+
// against a payload the first had already inflated.
|
|
1102
|
+
const staged = attemptCalibration.value;
|
|
1103
|
+
attemptCalibration.value = undefined;
|
|
1104
|
+
if (staged && !result.needsFallback) {
|
|
1105
|
+
recordKiroCalibration(staged.conversationId, staged.estimated, staged.charged);
|
|
1106
|
+
}
|
|
1010
1107
|
for (const event of deferred.splice(0)) {
|
|
1011
1108
|
try { yield event; } finally { retention.releaseEvent(event); }
|
|
1012
1109
|
}
|
|
@@ -1028,10 +1125,13 @@ async function* parseKiroAttemptEvents(
|
|
|
1028
1125
|
conversationId: string | undefined,
|
|
1029
1126
|
deferred: AdapterEvent[],
|
|
1030
1127
|
retention: KiroAttemptRetention,
|
|
1128
|
+
attemptCalibration: { value?: { conversationId: string; estimated: number; charged: number } },
|
|
1031
1129
|
contextInputEstimate?: number,
|
|
1032
1130
|
priorEmittedOutput = false,
|
|
1033
1131
|
): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
|
|
1034
1132
|
const emptyResult = (): KiroAttemptParseResult => ({ assistantText: "", sawReasoning: false });
|
|
1133
|
+
// Every early return below is a failure path that stages nothing; only the completion path
|
|
1134
|
+
// writes `attemptCalibration`, and the wrapper decides whether to commit it.
|
|
1035
1135
|
if (!response.body) {
|
|
1036
1136
|
return {
|
|
1037
1137
|
...emptyResult(),
|
|
@@ -1346,7 +1446,13 @@ async function* parseKiroAttemptEvents(
|
|
|
1346
1446
|
if (ev.stopReason !== undefined) stopReason = ev.stopReason;
|
|
1347
1447
|
break;
|
|
1348
1448
|
case "message_metadata":
|
|
1349
|
-
if (isValidKiroConversationId(ev.conversationId))
|
|
1449
|
+
if (isValidKiroConversationId(ev.conversationId)) {
|
|
1450
|
+
// Kiro can answer under a different conversation id than the request was built with.
|
|
1451
|
+
// Carry the calibration entry across so the record below finds its own raw estimate
|
|
1452
|
+
// instead of silently falling back to the already-corrected value.
|
|
1453
|
+
rekeyKiroCalibration(returnedConversationId, ev.conversationId);
|
|
1454
|
+
returnedConversationId = ev.conversationId;
|
|
1455
|
+
}
|
|
1350
1456
|
break;
|
|
1351
1457
|
case "content":
|
|
1352
1458
|
if (ev.modelId) {
|
|
@@ -1475,6 +1581,29 @@ async function* parseKiroAttemptEvents(
|
|
|
1475
1581
|
...(contextWindowState.value ? { upstreamContextWindow: contextWindowState.value } : {}),
|
|
1476
1582
|
});
|
|
1477
1583
|
}
|
|
1584
|
+
// Upstream just told us what this payload cost. The ratio between that and our pre-request
|
|
1585
|
+
// estimate is this conversation's own measured error, and it is the only feedback the
|
|
1586
|
+
// estimator ever receives.
|
|
1587
|
+
//
|
|
1588
|
+
// Staged, not recorded. An attempt that sets `needsFallback` is not over: the adapter rebuilds
|
|
1589
|
+
// the payload and streams a second time for the SAME user turn. Learning here would apply the
|
|
1590
|
+
// fresh factor to that rebuild and then learn again from it, so one turn would move the factor
|
|
1591
|
+
// twice and the second observation would score a payload the first had already inflated. Only
|
|
1592
|
+
// the outer parser knows whether an attempt is terminal, so it commits.
|
|
1593
|
+
//
|
|
1594
|
+
// Subtract the output first. `contextUsageTotalFloor` is the absolute context size AFTER the
|
|
1595
|
+
// response (`OcxUsage.contextTotalTokens`, types/request.ts), while `contextInputEstimate`
|
|
1596
|
+
// covers the request payload alone. Dividing one by the other would charge generated tokens to
|
|
1597
|
+
// prompt-tokenization error, so a short prompt answered at length would learn a large factor
|
|
1598
|
+
// and inflate every later request in that conversation — the premature compaction this work
|
|
1599
|
+
// exists to prevent.
|
|
1600
|
+
const chargedTotal = contextUsageTotalFloor();
|
|
1601
|
+
if (chargedTotal !== undefined && contextInputEstimate !== undefined) {
|
|
1602
|
+
const chargedInput = chargedTotal - finalUsage.outputTokens;
|
|
1603
|
+
if (chargedInput > 0 && returnedConversationId) {
|
|
1604
|
+
attemptCalibration.value = { conversationId: returnedConversationId, estimated: contextInputEstimate, charged: chargedInput };
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1478
1607
|
// Native stop metadata proves that this inference ended, but it does not prove that ordinary
|
|
1479
1608
|
// text is a final answer. Kiro has emitted END_TURN for progress prose, so tool-enabled turns
|
|
1480
1609
|
// still require the private completion call to distinguish commentary from completion (#531).
|
|
@@ -1934,7 +2063,10 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
|
|
|
1934
2063
|
if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
|
|
1935
2064
|
const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient);
|
|
1936
2065
|
await normalizeKiroImages(built.payload);
|
|
1937
|
-
|
|
2066
|
+
// Apply what earlier turns of THIS conversation measured. An unseen conversation is
|
|
2067
|
+
// unchanged, so a first turn behaves exactly as it would without calibration.
|
|
2068
|
+
const rawContextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId);
|
|
2069
|
+
const contextInputEstimate = calibrateKiroEstimate(built.conversationId, rawContextInputEstimate);
|
|
1938
2070
|
const body = JSON.stringify(built.payload);
|
|
1939
2071
|
debugProviderDiagnostic("kiro", "request", {
|
|
1940
2072
|
region,
|