Skip to content

Commit 7bb72e3

Browse files
committed
Merge remote-tracking branch 'upstream/dev' into restore/773-openai-chat-eof
2 parents 600ef52 + e03bf35 commit 7bb72e3

6 files changed

Lines changed: 119 additions & 20 deletions

File tree

src/adapters/kiro-tools.ts

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import type { OcxParsedRequest } from "../types";
1+
import type { OcxParsedRequest, OcxTool } from "../types";
22
import { namespacedToolName } from "../types";
33
import { normalizeKiroModelId } from "../providers/kiro-models";
44
import { createKiroToolNameRegistry, type KiroToolNameRegistry } from "./kiro-wire";
55

66
const MAX_KIRO_TOOL_DESCRIPTION_UNVERIFIED = 1024;
77
const MAX_KIRO_TOOL_DESCRIPTION_GPT_56_SOL = 9_216;
8+
// Issue #719's successful Kiro probe used 49 outbound tools at about 108 KiB. Leave one of those
9+
// slots for Kiro's private completion tool and headroom for the enclosing request/message fields.
10+
export const MAX_KIRO_TOOL_COUNT = 48;
11+
export const MAX_KIRO_TOOL_CATALOG_BYTES = 96_000;
12+
const textEncoder = new TextEncoder();
813

914
// JSON Schema validation/annotation keywords that Kiro's runtimeservice tool-spec validator
1015
// rejects ("ValidationException: Invalid tool use format."). Codex's built-in tools omit these,
@@ -147,6 +152,17 @@ function truncateDescription(description: string, limit: number): string {
147152
return `${description.slice(0, limit - 1)}…`;
148153
}
149154

155+
function serializedToolCatalogBytes(tools: readonly unknown[]): number {
156+
return textEncoder.encode(JSON.stringify(tools)).byteLength;
157+
}
158+
159+
function omittedToolCatalogNotice(kept: number, omitted: readonly OcxTool[], registry: KiroToolNameRegistry): string {
160+
const names = omitted.slice(0, 12).map(tool => registry.alias(namespacedToolName(tool.namespace, tool.name)));
161+
const remainder = omitted.length - names.length;
162+
const summary = `${names.join(", ")}${remainder > 0 ? `, and ${remainder} more` : ""}`;
163+
return `[opencodex] Kiro's outbound catalog budget allows ${kept} of ${kept + omitted.length} client tools this turn. Omitted and unavailable this turn: ${summary}.`;
164+
}
165+
150166
export function convertKiroToolContext(
151167
parsed: OcxParsedRequest,
152168
registry: KiroToolNameRegistry = createKiroToolNameRegistry(),
@@ -156,24 +172,39 @@ export function convertKiroToolContext(
156172
// Validate every listed name even when tool_choice:none emulates a tool-free turn.
157173
for (const tool of tools) registry.alias(namespacedToolName(tool.namespace, tool.name));
158174
const effectiveTools = parsed.options.toolChoice === "none" ? [] : tools;
175+
const convertedTools: unknown[] = [];
176+
let omittedAt = effectiveTools.length;
177+
for (const [index, tool] of effectiveTools.entries()) {
178+
const description = tool.description || `Tool: ${tool.name}`;
179+
// Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
180+
// it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
181+
// routes by. Kiro's runtimeservice rejects names with spaces or >64 chars, so normalize to a
182+
// safe form and remember the mapping; the response parser restores the original wire name.
183+
const wireName = namespacedToolName(tool.namespace, tool.name);
184+
const toolName = registry.alias(wireName);
185+
const converted = {
186+
toolSpecification: {
187+
name: toolName,
188+
description: truncateDescription(description, descriptionLimit),
189+
inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(tool.parameters ?? {})) },
190+
},
191+
};
192+
// Preserve declaration order and only omit a suffix. Ranking tools would make a catalog change
193+
// silently alter which capability disappears; this deterministic policy is paired with a
194+
// model-visible omission notice so unavailable tools are explicit rather than assumed absent.
195+
if (
196+
convertedTools.length >= MAX_KIRO_TOOL_COUNT
197+
|| serializedToolCatalogBytes([...convertedTools, converted]) > MAX_KIRO_TOOL_CATALOG_BYTES
198+
) {
199+
omittedAt = index;
200+
break;
201+
}
202+
convertedTools.push(converted);
203+
}
204+
const omittedTools = effectiveTools.slice(omittedAt);
159205
return {
160-
tools: effectiveTools.map(t => {
161-
const description = t.description || `Tool: ${t.name}`;
162-
// Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
163-
// it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
164-
// routes by. Kiro's runtimeservice rejects names with spaces or >64 chars, so normalize to a
165-
// safe form and remember the mapping; the response parser restores the original wire name.
166-
const wireName = namespacedToolName(t.namespace, t.name);
167-
const toolName = registry.alias(wireName);
168-
return {
169-
toolSpecification: {
170-
name: toolName,
171-
description: truncateDescription(description, descriptionLimit),
172-
inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(t.parameters ?? {})) },
173-
},
174-
};
175-
}),
176-
systemAdditions: [],
206+
tools: convertedTools,
207+
systemAdditions: omittedTools.length > 0 ? [omittedToolCatalogNotice(convertedTools.length, omittedTools, registry)] : [],
177208
nameMap: registry.nameMap,
178209
registry,
179210
};

src/adapters/kiro.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,10 @@ export function buildKiroPayload(
429429
// Neutralize Codex's GPT-5 identity line so a routed Kiro model never misreports as GPT-5/OpenAI
430430
// and the proxy identity never leaks upstream.
431431
if (parsed.context.systemPrompt?.length) systemParts.push(neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")));
432+
for (const addition of toolContext.systemAdditions) {
433+
const boundedAddition = boundedInjectedInstruction(addition, injectedChars);
434+
if (boundedAddition) systemParts.push(boundedAddition);
435+
}
432436
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeFromNames(kiroToolWireNames(kiroTools));
433437
const boundedNudge = toolCatalogNudge ? boundedInjectedInstruction(toolCatalogNudge, injectedChars) : undefined;
434438
if (boundedNudge) systemParts.push(boundedNudge);

src/providers/base-url-choices.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ export const ALIBABA_INTL_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
3939
{ id: "custom", label: "Custom" },
4040
];
4141

42+
/** Alibaba Coding Plan endpoint presets (international default; China mainland selectable). */
43+
export const ALIBABA_CODING_INTL_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1";
44+
export const ALIBABA_CODING_CN_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1";
45+
46+
export const ALIBABA_CODING_BASE_URL_CHOICES: readonly ProviderBaseUrlChoice[] = [
47+
{ id: "intl", label: "International", baseUrl: ALIBABA_CODING_INTL_BASE_URL },
48+
{ id: "china", label: "China", baseUrl: ALIBABA_CODING_CN_BASE_URL },
49+
{ id: "custom", label: "Custom" },
50+
];
51+
4252
/** Match a saved baseUrl to a known choice id (`custom` when it does not match). */
4353
export function matchBaseUrlChoice(
4454
choices: readonly ProviderBaseUrlChoice[],

src/providers/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ProviderBaseUrlChoice } from "./base-url-choices";
55
import {
66
QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
77
ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL,
8+
ALIBABA_CODING_BASE_URL_CHOICES, ALIBABA_CODING_INTL_BASE_URL,
89
} from "./base-url-choices";
910
import {
1011
CURSOR_STATIC_MODELS,
@@ -969,7 +970,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
969970
// 2026-07-10: docs unverified; model data frozen. Evidence: devlog/_plan/260710_provider_hardening/002_research_cn.md.
970971
{ id: "qianfan", label: "Qianfan (Baidu)", baseUrl: "https://qianfan.baidubce.com/v2", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://console.bce.baidu.com/iam/#/iam/apikey/list" },
971972
// 2026-07-10: docs unverified; model data frozen. Evidence: devlog/_plan/260710_provider_hardening/002_research_cn.md.
972-
{ id: "alibaba", label: "Alibaba Coding Plan", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://dashscope.console.aliyun.com/apiKey" },
973+
{ id: "alibaba", label: "Alibaba Coding Plan", baseUrl: ALIBABA_CODING_INTL_BASE_URL, adapter: "openai-chat", authKind: "key", allowBaseUrlOverride: true, baseUrlChoices: ALIBABA_CODING_BASE_URL_CHOICES, dashboardUrl: "https://dashscope.console.aliyun.com/apiKey" },
973974
{
974975
id: "alibaba-token-plan",
975976
label: "Alibaba Token Plan (Beijing)",

tests/kiro-adapter.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
55
import { join } from "node:path";
66
import { createKiroAdapter } from "../src/adapters/kiro";
77
import { KIRO_TOOL_RESULT_CARRIER_MESSAGE } from "../src/adapters/kiro-constants";
8+
import { MAX_KIRO_TOOL_CATALOG_BYTES, MAX_KIRO_TOOL_COUNT } from "../src/adapters/kiro-tools";
89
import { applyProviderConfigHints, buildCatalogEntries } from "../src/codex/catalog";
910
import { getValidAccessTokenSnapshot } from "../src/oauth";
1011
import { saveCredential } from "../src/oauth/store";
@@ -674,6 +675,53 @@ describe("kiro adapter — buildRequest", () => {
674675
expect(verifiedSpec.description.endsWith("…")).toBe(true);
675676
});
676677

678+
test("large catalogs retain the declared prefix within Kiro's count budget", async () => {
679+
// Each top-level description is below the existing per-description cap: this proves the
680+
// aggregate count budget, rather than that older truncation behavior, limits the catalog.
681+
const tools = Array.from({ length: MAX_KIRO_TOOL_COUNT + 20 }, (_, index) => ({
682+
name: `count_tool_${String(index).padStart(3, "0")}`,
683+
description: `Brief description ${index}`,
684+
parameters: { type: "object" },
685+
}));
686+
const current = JSON.parse((await createKiroAdapter(provider).buildRequest(
687+
parsedWith([{ role: "user", content: "hi" }], tools),
688+
)).body).conversationState.currentMessage.userInputMessage;
689+
const ordinary = current.userInputMessageContext.tools.slice(0, -1);
690+
691+
expect(ordinary).toHaveLength(MAX_KIRO_TOOL_COUNT);
692+
expect(ordinary.map((tool: { toolSpecification: { name: string } }) => tool.toolSpecification.name)).toEqual(
693+
tools.slice(0, MAX_KIRO_TOOL_COUNT).map(tool => tool.name),
694+
);
695+
expect(current.content).toContain(`Kiro's outbound catalog budget allows ${MAX_KIRO_TOOL_COUNT} of ${tools.length} client tools`);
696+
expect(current.content).toContain("count_tool_048");
697+
expect(current.content).toContain("Omitted and unavailable this turn");
698+
});
699+
700+
test("large catalogs retain the declared prefix within Kiro's serialized byte budget", async () => {
701+
// Top-level descriptions stay small, so existing description truncation cannot make this pass.
702+
// The repeated schema descriptions instead make the aggregate converted catalog exceed 96 KiB.
703+
const tools = Array.from({ length: 40 }, (_, index) => ({
704+
name: `byte_tool_${String(index).padStart(3, "0")}`,
705+
description: `Brief description ${index}`,
706+
parameters: {
707+
type: "object",
708+
properties: { payload: { type: "string", description: "x".repeat(8_000) } },
709+
},
710+
}));
711+
const current = JSON.parse((await createKiroAdapter(provider).buildRequest(
712+
parsedWith([{ role: "user", content: "hi" }], tools),
713+
)).body).conversationState.currentMessage.userInputMessage;
714+
const ordinary = current.userInputMessageContext.tools.slice(0, -1);
715+
const serializedBytes = new TextEncoder().encode(JSON.stringify(ordinary)).byteLength;
716+
717+
expect(ordinary.length).toBeLessThan(tools.length);
718+
expect(serializedBytes).toBeLessThanOrEqual(MAX_KIRO_TOOL_CATALOG_BYTES);
719+
expect(ordinary.map((tool: { toolSpecification: { name: string } }) => tool.toolSpecification.name)).toEqual(
720+
tools.slice(0, ordinary.length).map(tool => tool.name),
721+
);
722+
expect(current.content).toContain(`Kiro's outbound catalog budget allows ${ordinary.length} of ${tools.length} client tools`);
723+
});
724+
677725
test("historical tool calls stay structured when the current catalog is omitted", async () => {
678726
const messages = [
679727
{ role: "user", content: "run it" },

tests/provider-registry-parity.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ describe("provider registry parity", () => {
252252
label: "Alibaba Coding Plan",
253253
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
254254
});
255+
expect(PROVIDER_REGISTRY.find(entry => entry.id === "alibaba")?.baseUrlChoices).toEqual([
256+
{ id: "intl", label: "International", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1" },
257+
{ id: "china", label: "China", baseUrl: "https://coding.dashscope.aliyuncs.com/v1" },
258+
{ id: "custom", label: "Custom" },
259+
]);
255260
expect(KEY_LOGIN_PROVIDERS["alibaba-token-plan"]).toMatchObject({
256261
label: "Alibaba Token Plan (Beijing)",
257262
adapter: "openai-chat",
@@ -479,7 +484,7 @@ describe("provider registry parity", () => {
479484
test("base URL override permission is registry-only and limited to opted-in providers", () => {
480485
const optedIn = PROVIDER_REGISTRY.filter(entry => entry.allowBaseUrlOverride);
481486

482-
expect(optedIn.map(entry => entry.id)).toEqual(["ollama", "vllm", "lm-studio", "qwen-cloud", "alibaba-token-plan-intl", "litellm"]);
487+
expect(optedIn.map(entry => entry.id)).toEqual(["ollama", "vllm", "lm-studio", "qwen-cloud", "alibaba", "alibaba-token-plan-intl", "litellm"]);
483488
for (const entry of optedIn) {
484489
expect(providerConfigSeed(entry)).not.toHaveProperty("allowBaseUrlOverride");
485490
}

0 commit comments

Comments
 (0)