diff --git a/src/adapters/azure.ts b/src/adapters/azure.ts index fa2e428cf..a88d898a0 100644 --- a/src/adapters/azure.ts +++ b/src/adapters/azure.ts @@ -2,15 +2,17 @@ import type { IncomingMeta, ProviderAdapter } from "./base"; import type { OcxParsedRequest, OcxProviderConfig } from "../types"; import { createResponsesPassthroughAdapter } from "./openai-responses"; -export function createAzureAdapter(provider: OcxProviderConfig): ProviderAdapter & { passthrough: true } { - const inner = createResponsesPassthroughAdapter({ +export function createAzureAdapter( + provider: OcxProviderConfig, + inner: ProviderAdapter = createResponsesPassthroughAdapter({ ...provider, baseUrl: provider.baseUrl, - }); - + }), +): ProviderAdapter & { passthrough: true } { return { ...inner, name: "azure-openai", + passthrough: true, async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta) { if (provider.authMode === "forward") { diff --git a/src/adapters/contracts.ts b/src/adapters/contracts.ts new file mode 100644 index 000000000..8dc512b75 --- /dev/null +++ b/src/adapters/contracts.ts @@ -0,0 +1,98 @@ +import type { OcxProviderConfig } from "../types"; +import type { ProviderAdapter } from "./base"; + +export const REQUIRED_ROUTED_TOOL_CONTRACTS = [ + "tools.code-mode-nested-helper", + "tools.freeform-exact-roundtrip", + "tools.tool-choice-final-catalog", + "tools.continuation-replay", +] as const; + +export type RequiredRoutedToolContract = typeof REQUIRED_ROUTED_TOOL_CONTRACTS[number]; + +export type MutationContract = + | "mutation.codex-owned" + | "mutation.codex-owned-with-gated-native-fallback"; + +export type AdapterWire = + | "openai-chat" + | "anthropic" + | "google" + | "command-code" + | "kiro" + | "openai-responses" + | "cursor"; + +export type AdapterCacheRetention = "none" | "short" | "long"; + +export interface AdapterFactoryContext { + cacheRetention?: AdapterCacheRetention; +} + +export type AdapterFactory = ( + provider: OcxProviderConfig, + context: AdapterFactoryContext, +) => ProviderAdapter; + +export type AdapterWrapperFactory = ( + parent: ProviderAdapter, + provider: OcxProviderConfig, + context: AdapterFactoryContext, +) => ProviderAdapter; + +export interface DirectAdapterDefinitionInput { + kind: "direct"; + wire: AdapterWire; + mutation: MutationContract; + create: AdapterFactory; +} + +export interface WrappedAdapterDefinitionInput { + kind: "wrapper"; + extends: string; + wrap: AdapterWrapperFactory; +} + +export type AdapterDefinitionInput = DirectAdapterDefinitionInput | WrappedAdapterDefinitionInput; + +export type AdapterDefinition = Readonly< + T & { requiredToolContracts: typeof REQUIRED_ROUTED_TOOL_CONTRACTS } +>; + +type ValidAdapterRegistry> = { + [K in keyof T]: T[K] extends WrappedAdapterDefinitionInput + ? T[K]["extends"] extends Exclude + ? T[K] + : never + : T[K]; +}; + +type AssertNever = T; +type _UnknownWrapperParentIsRejected = AssertNever< + ValidAdapterRegistry<{ + base: DirectAdapterDefinitionInput; + broken: WrappedAdapterDefinitionInput & { extends: "not-registered" }; + }>["broken"] +>; +type _SelfWrapperParentIsRejected = AssertNever< + ValidAdapterRegistry<{ + self: WrappedAdapterDefinitionInput & { extends: "self" }; + }>["self"] +>; + +export function defineAdapterRegistry>( + definitions: T & ValidAdapterRegistry, +): { readonly [K in keyof T]: AdapterDefinition } { + const registered = Object.fromEntries( + Object.entries(definitions).map(([id, definition]) => [ + id, + Object.freeze({ + ...definition, + requiredToolContracts: REQUIRED_ROUTED_TOOL_CONTRACTS, + }), + ]), + ); + return Object.freeze(registered) as unknown as { + readonly [K in keyof T]: AdapterDefinition; + }; +} diff --git a/src/adapters/mimo-free.ts b/src/adapters/mimo-free.ts index b02ea0d20..eade07b9b 100644 --- a/src/adapters/mimo-free.ts +++ b/src/adapters/mimo-free.ts @@ -191,10 +191,20 @@ export function injectMimoSystemMarker(body: unknown): unknown { * 1. JWT from the bootstrap endpoint (cached, auto-refreshed). * 2. Anti-abuse system marker in the request body. * 3. Required headers (User-Agent, X-Mimo-Source, x-session-affinity). - * On 401/403, flushes the JWT cache and retries once via fetchResponse. + * On 401, flushes the JWT cache and retries once via fetchResponse. */ -export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdapter { - const base = createOpenAIChatAdapter(provider); +export interface MimoFreeAdapterDeps { + getJwt?: (signal?: AbortSignal) => Promise; + resetJwt?: () => void; +} + +export function createMimoFreeAdapter( + provider: OcxProviderConfig, + deps: MimoFreeAdapterDeps = {}, + base: ProviderAdapter = createOpenAIChatAdapter(provider), +): ProviderAdapter { + const getJwt = deps.getJwt ?? getMimoJwt; + const resetJwt = deps.resetJwt ?? resetMimoJwtCache; // Per-adapter session-affinity id (random, per process instance). const sessionId = `ses_${Math.random().toString(36).slice(2, 26)}`; @@ -203,7 +213,7 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap name: "mimo-free", async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): Promise { - const jwt = await getMimoJwt(); + const jwt = await getJwt(incoming?.abortSignal); // Let the base adapter build the wire body (handles reasoning, tools, etc.) // but override the URL and headers after. @@ -243,8 +253,8 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap if (response.status === 401) { // Drain the first response body before issuing the retry. try { await response.body?.cancel(); } catch { /* already consumed */ } - resetMimoJwtCache(); - const freshJwt = await getMimoJwt(ctx?.abortSignal); + resetJwt(); + const freshJwt = await getJwt(ctx?.abortSignal); const retryHeaders = { ...(request.headers as Record), "Authorization": `Bearer ${freshJwt}`, diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts new file mode 100644 index 000000000..e69204e17 --- /dev/null +++ b/src/adapters/registry.ts @@ -0,0 +1,166 @@ +import { createAnthropicAdapter } from "./anthropic"; +import { createAzureAdapter } from "./azure"; +import type { ProviderAdapter } from "./base"; +import { createCursorAdapter, type CursorAdapterDeps } from "./cursor"; +import { createGoogleAdapter } from "./google"; +import { createKiroAdapter } from "./kiro"; +import { createMimoFreeAdapter, type MimoFreeAdapterDeps } from "./mimo-free"; +import { createOpenAIChatAdapter } from "./openai-chat"; +import { createCommandCodeAdapter } from "./command-code"; +import { createResponsesPassthroughAdapter } from "./openai-responses"; +import type { OcxProviderConfig } from "../types"; +import { + defineAdapterRegistry, + type AdapterDefinition, + type AdapterFactoryContext, + type DirectAdapterDefinitionInput, +} from "./contracts"; + +type RegistryFactoryContext = AdapterFactoryContext & { + cursorDeps?: CursorAdapterDeps; + mimoDeps?: MimoFreeAdapterDeps; +}; + +function createRegisteredCursorAdapter(provider: OcxProviderConfig, context: AdapterFactoryContext) { + return createCursorAdapter(provider, (context as RegistryFactoryContext).cursorDeps); +} + +function wrapRegisteredMimoFreeAdapter( + parent: ProviderAdapter, + provider: OcxProviderConfig, + context: AdapterFactoryContext, +) { + return createMimoFreeAdapter( + provider, + (context as RegistryFactoryContext).mimoDeps, + parent, + ); +} + +export const ADAPTER_REGISTRY = defineAdapterRegistry({ + "command-code": { + kind: "direct", + wire: "command-code", + mutation: "mutation.codex-owned", + create: provider => createCommandCodeAdapter(provider), + }, + "openai-chat": { + kind: "direct", + wire: "openai-chat", + mutation: "mutation.codex-owned", + create: provider => createOpenAIChatAdapter(provider), + }, + anthropic: { + kind: "direct", + wire: "anthropic", + mutation: "mutation.codex-owned", + create: (provider, context) => createAnthropicAdapter(provider, context.cacheRetention), + }, + "openai-responses": { + kind: "direct", + wire: "openai-responses", + mutation: "mutation.codex-owned", + create: provider => createResponsesPassthroughAdapter(provider), + }, + google: { + kind: "direct", + wire: "google", + mutation: "mutation.codex-owned", + create: provider => createGoogleAdapter(provider), + }, + kiro: { + kind: "direct", + wire: "kiro", + mutation: "mutation.codex-owned", + create: provider => createKiroAdapter(provider), + }, + azure: { + kind: "wrapper", + extends: "openai-responses", + wrap: (parent, provider) => createAzureAdapter(provider, parent), + }, + "azure-openai": { + kind: "wrapper", + extends: "openai-responses", + wrap: (parent, provider) => createAzureAdapter(provider, parent), + }, + cursor: { + kind: "direct", + wire: "cursor", + mutation: "mutation.codex-owned-with-gated-native-fallback", + create: createRegisteredCursorAdapter, + }, + "mimo-free": { + kind: "wrapper", + extends: "openai-chat", + wrap: wrapRegisteredMimoFreeAdapter, + }, +}); + +export type AdapterId = keyof typeof ADAPTER_REGISTRY; +export type RegisteredAdapterDefinition = typeof ADAPTER_REGISTRY[AdapterId]; +export type EffectiveAdapterContract = AdapterDefinition; + +export function adapterDefinitions(): Array<[AdapterId, RegisteredAdapterDefinition]> { + return Object.entries(ADAPTER_REGISTRY) as Array<[AdapterId, RegisteredAdapterDefinition]>; +} + +export function getAdapterDefinition(adapterId: string): RegisteredAdapterDefinition | undefined { + return ADAPTER_REGISTRY[adapterId as AdapterId]; +} + +export function effectiveAdapterContract(adapterId: string): EffectiveAdapterContract { + const visited = new Set(); + let current = adapterId; + + while (true) { + if (visited.has(current)) { + throw new Error(`Adapter wrapper cycle detected at ${current}`); + } + visited.add(current); + + const definition = getAdapterDefinition(current); + if (!definition) throw new Error(`Unknown adapter: ${current}`); + if (definition.kind === "direct") return definition; + current = definition.extends; + } +} + +function createRegisteredAdapterById( + adapterId: string, + provider: OcxProviderConfig, + context: RegistryFactoryContext, + visited: ReadonlySet, +): ProviderAdapter { + if (visited.has(adapterId)) { + throw new Error(`Adapter wrapper cycle detected at ${adapterId}`); + } + + const definition = getAdapterDefinition(adapterId); + if (!definition) throw new Error(`Unknown adapter: ${adapterId}`); + + const adapterProvider = provider.adapter === adapterId + ? provider + : { ...provider, adapter: adapterId }; + + if (definition.kind === "direct") { + return definition.create(adapterProvider, context); + } + + const nextVisited = new Set(visited); + nextVisited.add(adapterId); + const parent = createRegisteredAdapterById( + definition.extends, + adapterProvider, + context, + nextVisited, + ); + return definition.wrap(parent, adapterProvider, context); +} + +export function createRegisteredAdapter( + provider: OcxProviderConfig, + context: RegistryFactoryContext = {}, +): ProviderAdapter { + return createRegisteredAdapterById(provider.adapter, provider, context, new Set()); +} diff --git a/src/lab/conformance/executor.ts b/src/lab/conformance/executor.ts index dee649627..0d0c0a57b 100644 --- a/src/lab/conformance/executor.ts +++ b/src/lab/conformance/executor.ts @@ -1,5 +1,4 @@ -import { createOpenAIChatAdapter } from "../../adapters/openai-chat"; -import { createResponsesPassthroughAdapter } from "../../adapters/openai-responses"; +import { createRegisteredAdapter } from "../../adapters/registry"; import { bridgeToResponsesSSE, buildResponseJSON } from "../../bridge"; import { anthropicToResponsesTranslation } from "../../claude/inbound"; import { responsesSseToAnthropicSse } from "../../claude/outbound"; @@ -25,6 +24,14 @@ import { import { normalizeSseBytes } from "./sse-normalize"; import type { CaseRecord, NormalizedObservation, ScenarioRunResult, ProtocolExecutionContextV1 } from "./types"; +function createOpenAIChatAdapter(provider: OcxProviderConfig) { + return createRegisteredAdapter({ ...provider, adapter: "openai-chat" }); +} + +function createResponsesPassthroughAdapter(provider: OcxProviderConfig) { + return createRegisteredAdapter({ ...provider, adapter: "openai-responses" }); +} + export function resolveProtocolExecutionContext(caseRecord: CaseRecord): ProtocolExecutionContextV1 { const inbound = caseRecord.requirements.inboundProtocols[0] ?? "openai-responses"; const upstream = caseRecord.requirements.upstreamProtocols[0] ?? "openai-chat"; diff --git a/src/server/adapter-resolve.ts b/src/server/adapter-resolve.ts index 2edf7e3ee..9680cd358 100644 --- a/src/server/adapter-resolve.ts +++ b/src/server/adapter-resolve.ts @@ -1,16 +1,8 @@ -import { createAnthropicAdapter } from "../adapters/anthropic"; -import { createAzureAdapter } from "../adapters/azure"; -import { createCursorAdapter } from "../adapters/cursor"; -import { createGoogleAdapter } from "../adapters/google"; -import { createKiroAdapter } from "../adapters/kiro"; -import { createMimoFreeAdapter } from "../adapters/mimo-free"; -import { createOpenAIChatAdapter } from "../adapters/openai-chat"; -import { createCommandCodeAdapter } from "../adapters/command-code"; -import { createResponsesPassthroughAdapter } from "../adapters/openai-responses"; import type { OcxProviderConfig } from "../types"; import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types"; import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; import { type InboundWire, providerModelWireDefault } from "../providers/registry"; +import { createRegisteredAdapter } from "../adapters/registry"; /** * Resolve the wire a single model should use: a hard pin first, then a configured @@ -35,11 +27,7 @@ export function resolveWireProtocolOverride( if (pinned && providerConfig.adapter !== pinned) { return { ...providerConfig, adapter: pinned }; } - // Re-check the allow-list here, not just in the config validator: the file may have - // been hand-edited, or written by a build that allowed more values. const configured = providerConfig.modelAdapters?.[modelId]; - // An explicit allowed override wins, including one naming the provider-wide adapter (the - // opt-out from a registry default). Invalid hand-edited values fall through to the default. const requested = configured && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured) ? configured : providerModelWireDefault(providerName, providerConfig, modelId, MODEL_ADAPTER_OVERRIDE_ALLOWED, inbound); @@ -47,8 +35,6 @@ export function resolveWireProtocolOverride( && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(requested) && requested !== providerConfig.adapter && !isWirePinnedModel(providerName, modelId) - // A forward provider hands the caller's own credential upstream; the chat adapter - // only ever sends provider.apiKey, so switching wires here would drop the auth. && !isCanonicalOpenAiForwardProvider(providerConfig)) { return { ...providerConfig, adapter: requested }; } @@ -57,27 +43,5 @@ export function resolveWireProtocolOverride( /** Build the provider adapter for a resolved provider config. */ export function resolveAdapter(providerConfig: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") { - switch (providerConfig.adapter) { - case "command-code": - return createCommandCodeAdapter(providerConfig); - case "openai-chat": - return createOpenAIChatAdapter(providerConfig); - case "anthropic": - return createAnthropicAdapter(providerConfig, cacheRetention); - case "openai-responses": - return createResponsesPassthroughAdapter(providerConfig); - case "google": - return createGoogleAdapter(providerConfig); - case "kiro": - return createKiroAdapter(providerConfig); - case "azure": - case "azure-openai": - return createAzureAdapter(providerConfig); - case "cursor": - return createCursorAdapter(providerConfig); - case "mimo-free": - return createMimoFreeAdapter(providerConfig); - default: - throw new Error(`Unknown adapter: ${providerConfig.adapter}`); - } + return createRegisteredAdapter(providerConfig, { cacheRetention }); } diff --git a/tests/adapter-registry-boundary.test.ts b/tests/adapter-registry-boundary.test.ts new file mode 100644 index 000000000..e92f660a7 --- /dev/null +++ b/tests/adapter-registry-boundary.test.ts @@ -0,0 +1,114 @@ +import { expect, test } from "bun:test"; +import { readdir, readFile } from "node:fs/promises"; +import { join, posix, relative, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = fileURLToPath(new URL("../", import.meta.url)); +const adaptersDir = join(repoRoot, "src", "adapters"); +const serverDir = join(repoRoot, "src", "server"); +const routerPath = join(repoRoot, "src", "router.ts"); +const labExecutorPath = join(repoRoot, "src", "lab", "conformance", "executor.ts"); +const importScanner = new Bun.Transpiler({ loader: "ts" }); +const exportedAdapterFactory = /\bexport\s+(?:async\s+)?function\s+(create[A-Za-z0-9_$]*Adapter)\s*\(|\bexport\s+const\s+(create[A-Za-z0-9_$]*Adapter)\b/g; + +async function collectTypeScriptFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + for (const entry of entries) { + const path = join(dir, entry.name); + if (entry.isDirectory()) files.push(...await collectTypeScriptFiles(path)); + else if (entry.isFile() && entry.name.endsWith(".ts")) files.push(path); + } + return files; +} + +function adapterModuleId(path: string): string { + return relative(adaptersDir, path) + .split(sep) + .join("/") + .replace(/\.ts$/, ""); +} + +async function discoverAdapterFactories(): Promise> { + const factories = new Map(); + for (const path of await collectTypeScriptFiles(adaptersDir)) { + if (path === join(adaptersDir, "registry.ts")) continue; + const source = await readFile(path, "utf8"); + const names = new Set(); + for (const match of source.matchAll(exportedAdapterFactory)) { + const name = match[1] ?? match[2]; + if (name) names.add(name); + } + if (names.size > 0) factories.set(adapterModuleId(path), [...names]); + } + return factories; +} + +function importedAdapterModule( + specifier: string, + factories: ReadonlyMap, +): string | undefined { + const normalized = posix.normalize(specifier.replaceAll("\\", "/")); + const parts = normalized.split("/"); + const adapterIndex = parts.lastIndexOf("adapters"); + if (adapterIndex < 0 || adapterIndex === parts.length - 1) return undefined; + const moduleId = parts.slice(adapterIndex + 1).join("/").replace(/\.(?:[cm]?[jt]s)$/, ""); + return factories.has(moduleId) ? moduleId : undefined; +} + +function containsIdentifier(source: string, target: string): boolean { + const isStart = (code: number) => + (code >= 65 && code <= 90) + || (code >= 97 && code <= 122) + || code === 36 + || code === 95; + const isContinue = (code: number) => isStart(code) || (code >= 48 && code <= 57); + + for (let i = 0; i < source.length;) { + const code = source.charCodeAt(i); + if (!isStart(code)) { + i += 1; + continue; + } + let end = i + 1; + while (end < source.length && isContinue(source.charCodeAt(end))) end += 1; + if (source.slice(i, end) === target) return true; + i = end; + } + return false; +} + +async function directAdapterFactoryImports( + path: string, + factories: ReadonlyMap, +): Promise { + const source = await readFile(path, "utf8"); + const runtimeSource = importScanner.transformSync(source); + const findings: string[] = []; + + for (const entry of importScanner.scan(source).imports) { + const moduleId = importedAdapterModule(entry.path, factories); + if (!moduleId) continue; + if (entry.kind !== "import-statement") { + findings.push(`${entry.kind} from ${entry.path}`); + continue; + } + for (const factory of factories.get(moduleId) ?? []) { + if (containsIdentifier(runtimeSource, factory)) { + findings.push(`${factory} from ${entry.path}`); + } + } + } + return findings; +} + +test("routing boundaries discover adapter factories instead of maintaining a second inventory", async () => { + const factories = await discoverAdapterFactories(); + expect(factories.size).toBeGreaterThan(0); + expect(factories.get("openai-chat")).toContain("createOpenAIChatAdapter"); + + const files = [...await collectTypeScriptFiles(serverDir), routerPath, labExecutorPath]; + for (const path of files) { + expect(await directAdapterFactoryImports(path, factories), path).toEqual([]); + } +}); diff --git a/tests/adapter-registry-hardening.test.ts b/tests/adapter-registry-hardening.test.ts new file mode 100644 index 000000000..81f66cd38 --- /dev/null +++ b/tests/adapter-registry-hardening.test.ts @@ -0,0 +1,69 @@ +import { expect, test } from "bun:test"; +import type { AdapterRequest } from "../src/adapters/base"; +import { createMimoFreeAdapter } from "../src/adapters/mimo-free"; +import { adapterDefinitions } from "../src/adapters/registry"; +import type { OcxProviderConfig } from "../src/types"; + +test("wrapper registry entries cannot construct an independent adapter", () => { + const wrappers = adapterDefinitions().filter(([, definition]) => definition.kind === "wrapper"); + expect(wrappers.length).toBeGreaterThan(0); + + for (const [adapterId, definition] of wrappers) { + expect("create" in definition, adapterId).toBe(false); + expect("wrap" in definition, adapterId).toBe(true); + if (definition.kind === "wrapper") { + expect(typeof definition.wrap, adapterId).toBe("function"); + } + } +}); + +test("MiMo 401 retry invalidates the injected JWT source before reacquiring a token", async () => { + const originalFetch = globalThis.fetch; + const observedAuthorization: string[] = []; + let token = "stale-token"; + let resetCalls = 0; + let fetchCalls = 0; + + try { + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + fetchCalls += 1; + const headers = new Headers(init?.headers); + observedAuthorization.push(headers.get("authorization") ?? ""); + return fetchCalls === 1 + ? new Response("expired", { status: 401 }) + : new Response("ok", { status: 200 }); + }) as typeof fetch; + + const provider = { + adapter: "mimo-free", + baseUrl: "https://api.xiaomimimo.com/api/free-ai/openai", + authMode: "key", + apiKey: "unused", + defaultMaxOutputTokens: 64_000, + } as OcxProviderConfig; + const adapter = createMimoFreeAdapter(provider, { + getJwt: async () => token, + resetJwt: () => { + resetCalls += 1; + token = "fresh-token"; + }, + }); + const request: AdapterRequest = { + url: "https://api.xiaomimimo.com/api/free-ai/openai/chat", + method: "POST", + headers: { Authorization: "Bearer stale-token" }, + body: "{}", + }; + + expect(adapter.fetchResponse).toBeDefined(); + const response = await adapter.fetchResponse!(request, {}); + expect(response.status).toBe(200); + expect(resetCalls).toBe(1); + expect(observedAuthorization).toEqual([ + "Bearer stale-token", + "Bearer fresh-token", + ]); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/apply-patch-adapter-nudge-regression.test.ts b/tests/apply-patch-adapter-nudge-regression.test.ts new file mode 100644 index 000000000..25535f20d --- /dev/null +++ b/tests/apply-patch-adapter-nudge-regression.test.ts @@ -0,0 +1,121 @@ +import { expect, test } from "bun:test"; +import { createAnthropicAdapter } from "../src/adapters/anthropic"; +import { createCommandCodeAdapter } from "../src/adapters/command-code"; +import { createGoogleAdapter } from "../src/adapters/google"; +import { createKiroAdapter } from "../src/adapters/kiro"; +import { createOpenAIChatAdapter } from "../src/adapters/openai-chat"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; + +function codeModeParsed(modelId: string): OcxParsedRequest { + return { + modelId, + stream: true, + options: {}, + context: { + systemPrompt: ["Use apply_patch for local file edits."], + messages: [{ role: "user", content: "Patch a file.", timestamp: 1 }], + tools: [ + { + name: "exec", + description: "Run JavaScript. declare const tools: { apply_patch(input: string): Promise; };", + parameters: { type: "object", properties: { input: { type: "string" } } }, + }, + { + name: "wait", + description: "Wait for work to finish.", + parameters: { type: "object", properties: {} }, + }, + { + name: "request_user_input", + description: "Ask the user for input.", + parameters: { type: "object", properties: {} }, + }, + ], + }, + } as OcxParsedRequest; +} + +function assertApplyPatchIsNotForbidden(body: string): void { + const normalized = body.replace(/\\n/g, " ").replace(/\s+/g, " "); + + // Make sure the adapter actually exercised the shared catalog-nudge call site; + // otherwise a missing nudge would make the prohibition assertion vacuous. + expect(normalized).toContain("current tool catalog as ground truth"); + + // The final provider request must still advertise Codex's nested patch helper. + // This pins the actual Code Mode declaration rather than a nonexistent literal + // `tools.apply_patch` token in the serialized tool description. + expect(normalized).toContain("declare const tools: { apply_patch(input: string): Promise; };"); + + // Protect against both the original shared warning and adapter-specific wording + // that would steer routed models away from Codex's own patch tool. + expect(normalized).not.toMatch(/(?:do not|don't|never|must not|cannot|can't)[^.]{0,260}\bapply_patch\b/i); + expect(normalized).not.toMatch(/\bapply_patch\b[^.]{0,180}\b(?:forbidden|unavailable|off-limits)\b/i); +} + +test("routed adapter call sites never forbid Codex apply_patch", async () => { + const cases: Array<{ + name: string; + modelId: string; + build: (parsed: OcxParsedRequest) => Promise<{ body: string }>; + }> = [ + { + name: "openai-chat", + modelId: "grok-4.6", + build: async parsed => createOpenAIChatAdapter({ + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + apiKey: "test-key", + } as OcxProviderConfig).buildRequest(parsed), + }, + { + name: "anthropic-oauth", + modelId: "claude-haiku-4-5", + build: async parsed => createAnthropicAdapter({ + adapter: "anthropic", + baseUrl: "https://api.anthropic.com", + authMode: "oauth", + apiKey: "test-oauth-token", + } as OcxProviderConfig).buildRequest(parsed), + }, + { + name: "google", + modelId: "gemini-3-pro", + build: async parsed => createGoogleAdapter({ + adapter: "google", + baseUrl: "https://generativelanguage.googleapis.com", + apiKey: "test-key", + } as OcxProviderConfig).buildRequest(parsed), + }, + { + name: "command-code", + modelId: "deepseek/deepseek-v4-flash", + build: async parsed => createCommandCodeAdapter({ + adapter: "command-code", + baseUrl: "https://api.commandcode.ai", + authMode: "oauth", + apiKey: "test-command-key", + defaultMaxOutputTokens: 64_000, + } as OcxProviderConfig).buildRequest(parsed), + }, + { + name: "kiro", + modelId: "claude-sonnet-4.5", + build: async parsed => { + parsed._kiroAuthContext = { apiRegion: "us-east-1" }; + return createKiroAdapter({ + adapter: "kiro", + baseUrl: "https://runtime.us-east-1.kiro.dev", + authMode: "key", + apiKey: "ksk_test", + } as OcxProviderConfig).buildRequest(parsed); + }, + }, + ]; + + for (const adapterCase of cases) { + const request = await adapterCase.build(codeModeParsed(adapterCase.modelId)); + expect(request.body, `${adapterCase.name} should serialize a request body`).toBeTruthy(); + assertApplyPatchIsNotForbidden(request.body); + } +}); diff --git a/tests/apply-patch-catalog-contract.test.ts b/tests/apply-patch-catalog-contract.test.ts new file mode 100644 index 000000000..2d9482cd0 --- /dev/null +++ b/tests/apply-patch-catalog-contract.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "bun:test"; +import { ensureStrictCatalogFields, normalizeRoutedCatalogEntry } from "../src/codex/catalog/parsing"; + +test("routed catalog rows preserve an explicit apply_patch tool type", () => { + const row = normalizeRoutedCatalogEntry({ + slug: "xai/grok-4.6", + tool_mode: "legacy", + apply_patch_tool_type: "function", + context_window: 128_000, + }); + + expect(row.tool_mode).toBe("code_mode_only"); + expect(row.apply_patch_tool_type).toBe("function"); +}); + +test("routed catalog rows default a missing apply_patch tool type to freeform", () => { + const row = normalizeRoutedCatalogEntry({ + slug: "xai/grok-4.6", + tool_mode: "legacy", + context_window: 128_000, + }); + + expect(row.tool_mode).toBe("code_mode_only"); + expect(row.apply_patch_tool_type).toBe("freeform"); +}); + +test("native catalog rows preserve an explicit apply_patch tool type", () => { + const row = ensureStrictCatalogFields({ + slug: "gpt-5.6-sol", + apply_patch_tool_type: "function", + context_window: 128_000, + }); + + expect(row.apply_patch_tool_type).toBe("function"); +}); diff --git a/tests/apply-patch-code-mode-regression.test.ts b/tests/apply-patch-code-mode-regression.test.ts new file mode 100644 index 000000000..f7534de36 --- /dev/null +++ b/tests/apply-patch-code-mode-regression.test.ts @@ -0,0 +1,166 @@ +import { expect, test } from "bun:test"; +import { createOpenAIChatAdapter as createOpenAIChatAdapterProduction } from "../src/adapters/openai-chat"; +import { bridgeToResponsesSSE } from "../src/bridge"; +import { parseRequest } from "../src/responses/parser"; +import { buildToolBridgeMaps } from "../src/server/responses"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { withTestTranslatorBudget } from "./helpers/translator-budget"; + +const provider: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + apiKey: "test-key", +}; + +const createOpenAIChatAdapter = (...args: Parameters) => + withTestTranslatorBudget(createOpenAIChatAdapterProduction(...args)); + +function parseSse(text: string): Array<{ event?: string; data: Record }> { + return text.split("\n\n") + .map(frame => frame.trim()) + .filter(frame => frame.length > 0 && frame !== "data: [DONE]") + .map(frame => { + const lines = frame.split("\n"); + const event = lines.find(line => line.startsWith("event: "))?.slice(7); + const data = lines.find(line => line.startsWith("data: "))?.slice(6) ?? "{}"; + return { event, data: JSON.parse(data) as Record }; + }); +} + +test("routed Code Mode does not forbid nested apply_patch when it is absent from the flat catalog", () => { + const parsed: OcxParsedRequest = { + modelId: "grok-4.6", + context: { + systemPrompt: ["Use apply_patch for local file edits."], + messages: [{ role: "user", content: "Edit the requested file.", timestamp: 0 }], + tools: [ + { + name: "exec", + description: "Run JavaScript. declare const tools: { apply_patch(input: string): Promise; };", + parameters: {}, + }, + { name: "wait", description: "Wait for an exec cell.", parameters: {} }, + { name: "request_user_input", description: "Ask the user a question.", parameters: {} }, + ], + }, + stream: false, + options: {}, + }; + + const request = createOpenAIChatAdapter(provider).buildRequest(parsed); + const body = JSON.parse(request.body) as { + messages: Array<{ role: string; content: unknown }>; + tools?: Array<{ function?: { name?: string; description?: string } }>; + }; + + const systemText = body.messages + .filter(message => message.role === "system" && typeof message.content === "string") + .map(message => message.content as string) + .join("\n"); + const execTool = body.tools?.find(tool => tool.function?.name === "exec"); + + // Reproduce the actual Code Mode shape: patching exists only as a nested exec helper, + // not as a top-level wire tool. Before 0325a5a the injected nudge contradicted this + // contract by explicitly forbidding apply_patch, which pushed routed models to Python/sed. + expect(execTool?.function?.description).toContain("apply_patch"); + expect(body.tools?.some(tool => tool.function?.name === "apply_patch")).toBe(false); + expect(systemText).toContain("Use apply_patch for local file edits."); + expect(systemText).toContain("Valid tool names for this turn are exactly `exec`, `wait`, `request_user_input`"); + expect(systemText).toContain("Do not use neighboring-agent tool names"); + expect(systemText).not.toMatch(/Do not use neighboring-agent tool names[^.]*apply_patch/); +}); + +test("routed chat round-trips a declared apply_patch custom tool with valid freeform input", async () => { + const patch = [ + "*** Begin Patch", + "*** Add File: ocx-apply-patch-smoke.txt", + "+apply patch smoke", + "*** End Patch", + ].join("\n"); + const parsed = parseRequest({ + model: "xai/grok-4.6", + input: "Create the smoke-test file.", + stream: true, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }); + const maps = buildToolBridgeMaps(parsed); + const adapter = createOpenAIChatAdapter(provider); + + // Responses custom/freeform tools are lowered to a normal chat function with one string + // field. If this wrapper changes or disappears, routed chat models cannot call apply_patch. + const outbound = JSON.parse(adapter.buildRequest(parsed).body) as { + tools?: Array<{ + type?: string; + function?: { + name?: string; + parameters?: { properties?: { input?: { type?: string } } }; + }; + }>; + }; + expect(outbound.tools?.find(tool => tool.function?.name === "apply_patch")).toMatchObject({ + type: "function", + function: { + name: "apply_patch", + parameters: { properties: { input: { type: "string" } } }, + }, + }); + + // Simulate the routed chat model selecting that function. The adapter must parse the call, + // then the Responses bridge must unwrap {input:string} back into a native custom_tool_call. + const upstreamPayload = JSON.stringify({ + choices: [{ + delta: { + tool_calls: [{ + index: 0, + id: "call_patch", + type: "function", + function: { + name: "apply_patch", + arguments: JSON.stringify({ input: patch }), + }, + }], + }, + finish_reason: "tool_calls", + }], + }); + const upstream = new Response(`data: ${upstreamPayload}\n\ndata: [DONE]\n\n`); + const bridged = bridgeToResponsesSSE( + adapter.parseStream(upstream), + parsed.modelId, + maps.toolNsMap, + maps.freeformToolNames, + maps.toolSearchToolNames, + undefined, + 2_000, + { declaredToolNames: maps.declaredToolNames }, + ); + const frames = parseSse(await new Response(bridged).text()); + + const inputDone = frames.find(frame => frame.event === "response.custom_tool_call_input.done")?.data; + expect(inputDone?.input).toBe(patch); + + const itemDone = frames.find(frame => { + if (frame.event !== "response.output_item.done") return false; + const item = frame.data.item as Record | undefined; + return item?.type === "custom_tool_call" && item.name === "apply_patch"; + })?.data.item as Record | undefined; + expect(itemDone).toMatchObject({ + type: "custom_tool_call", + call_id: "call_patch", + name: "apply_patch", + input: patch, + status: "completed", + }); + + const completed = frames.find(frame => frame.event === "response.completed")?.data.response as + | { status?: string; output?: Array> } + | undefined; + expect(completed?.status).toBe("completed"); + expect(completed?.output).toContainEqual(expect.objectContaining({ + type: "custom_tool_call", + name: "apply_patch", + input: patch, + status: "completed", + })); + expect(frames.some(frame => frame.event === "response.failed")).toBe(false); +}); diff --git a/tests/apply-patch-conformance.test.ts b/tests/apply-patch-conformance.test.ts new file mode 100644 index 000000000..b7cff4029 --- /dev/null +++ b/tests/apply-patch-conformance.test.ts @@ -0,0 +1,769 @@ +import { create, fromBinary } from "@bufbuild/protobuf"; +import { expect, test } from "bun:test"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, posix } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + ADAPTER_REGISTRY, + adapterDefinitions, + createRegisteredAdapter as createProductionRegisteredAdapter, + effectiveAdapterContract, +} from "../src/adapters/registry"; +import { REQUIRED_ROUTED_TOOL_CONTRACTS } from "../src/adapters/contracts"; +import type { AdapterWire } from "../src/adapters/contracts"; +import { + AgentClientMessageSchema, + ExecServerMessageSchema, + WriteArgsSchema, +} from "../src/adapters/cursor/gen/agent_pb"; +import { handleCursorNativeExec } from "../src/adapters/cursor/native-exec"; +import { createCursorRequest } from "../src/adapters/cursor/request-builder"; +import { + cursorRequestAdvertisesApplyPatch, + isCursorSyntheticStructuredEditTool, +} from "../src/adapters/cursor/tool-definitions"; +import { bridgeToResponsesSSE, buildResponseJSON } from "../src/bridge"; +import { ensureStrictCatalogFields, normalizeRoutedCatalogEntry } from "../src/codex/catalog/parsing"; +import { PROVIDER_REGISTRY } from "../src/providers/registry"; +import { parseRequest } from "../src/responses/parser"; +import { buildToolBridgeMaps } from "../src/server/responses"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../src/types"; +import { + APPLY_PATCH_FIXTURE, + applyPatchContractFailures, + type ApplyPatchContractId, + type ApplyPatchObservation, +} from "./helpers/apply-patch-conformance/contracts"; +import { TOOL_WIRE_DRIVERS } from "./helpers/apply-patch-conformance/wire-drivers"; +import { createTestTranslatorBudget } from "./helpers/translator-budget"; + +const serverDir = fileURLToPath(new URL("../src/server/", import.meta.url)); +const adapterResolvePath = fileURLToPath(new URL("../src/server/adapter-resolve.ts", import.meta.url)); +const routerPath = fileURLToPath(new URL("../src/router.ts", import.meta.url)); +const labExecutorPath = fileURLToPath(new URL("../src/lab/conformance/executor.ts", import.meta.url)); +const execDescription = + "Run JavaScript. declare const tools: { apply_patch(input: string): Promise; };"; + +const WIRE_MODELS: Record = { + "openai-chat": "grok-4.6", + anthropic: "claude-haiku-4-5", + google: "gemini-3.5-flash", + "command-code": "deepseek/deepseek-v4-flash", + kiro: "claude-sonnet-4.5", + "openai-responses": "deepseek-v4-flash", + cursor: "cursor/auto", +}; + +const EXTERNAL_EXACT_ROUNDTRIP_COVERAGE = new Set([ + "openai-responses", + "cursor", +]); + +async function collectTypeScriptFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const files: string[] = []; + for (const entry of entries) { + const path = join(dir, entry.name); + if (entry.isDirectory()) files.push(...await collectTypeScriptFiles(path)); + else if (entry.isFile() && entry.name.endsWith(".ts")) files.push(path); + } + return files; +} + +const ADAPTER_FACTORY_EXPORTS = { + anthropic: "createAnthropicAdapter", + azure: "createAzureAdapter", + "command-code": "createCommandCodeAdapter", + cursor: "createCursorAdapter", + google: "createGoogleAdapter", + kiro: "createKiroAdapter", + "mimo-free": "createMimoFreeAdapter", + "openai-chat": "createOpenAIChatAdapter", + "openai-responses": "createResponsesPassthroughAdapter", +} as const; + +type DirectAdapterModule = keyof typeof ADAPTER_FACTORY_EXPORTS; + +const importScanner = new Bun.Transpiler({ loader: "ts" }); + +function directAdapterModule(specifier: string): DirectAdapterModule | undefined { + const normalized = posix.normalize(specifier.replaceAll("\\", "/")); + const parts = normalized.split("/"); + const adapterIndex = parts.lastIndexOf("adapters"); + if (adapterIndex < 0 || adapterIndex !== parts.length - 2) return undefined; + const leaf = parts.at(-1)!; + const extensionIndex = leaf.lastIndexOf("."); + const moduleName = extensionIndex > 0 ? leaf.slice(0, extensionIndex) : leaf; + return moduleName in ADAPTER_FACTORY_EXPORTS + ? moduleName as DirectAdapterModule + : undefined; +} + +function containsIdentifier(source: string, target: string): boolean { + const isStart = (code: number) => + (code >= 65 && code <= 90) + || (code >= 97 && code <= 122) + || code === 36 + || code === 95; + const isContinue = (code: number) => isStart(code) || (code >= 48 && code <= 57); + + for (let i = 0; i < source.length;) { + const code = source.charCodeAt(i); + if (!isStart(code)) { + i += 1; + continue; + } + let end = i + 1; + while (end < source.length && isContinue(source.charCodeAt(end))) end += 1; + if (source.slice(i, end) === target) return true; + i = end; + } + return false; +} + +async function directAdapterFactoryImports(path: string): Promise { + const source = await readFile(path, "utf8"); + const runtimeSource = importScanner.transformSync(source); + const findings: string[] = []; + + for (const entry of importScanner.scan(source).imports) { + const moduleName = directAdapterModule(entry.path); + if (!moduleName) continue; + if (entry.kind !== "import-statement") { + findings.push(`${entry.kind} from ${entry.path}`); + continue; + } + const factory = ADAPTER_FACTORY_EXPORTS[moduleName]; + if (containsIdentifier(runtimeSource, factory)) { + findings.push(`${factory} from ${entry.path}`); + } + } + return findings; +} + +function providerFixture(adapter: string, wire: AdapterWire): OcxProviderConfig { + const baseUrls: Record = { + "openai-chat": "https://api.x.ai/v1", + anthropic: "https://api.anthropic.com", + google: "https://generativelanguage.googleapis.com", + "command-code": "https://api.commandcode.ai", + kiro: "https://runtime.us-east-1.kiro.dev", + "openai-responses": "https://api.deepseek.com", + cursor: "https://api2.cursor.sh", + }; + return { + adapter, + baseUrl: baseUrls[wire], + authMode: wire === "anthropic" || wire === "command-code" ? "oauth" : "key", + apiKey: wire === "kiro" ? "ksk_test" : "test-key", + defaultMaxOutputTokens: 64_000, + googleMode: "ai-studio", + ...(wire === "openai-responses" ? { responsesPath: "/responses" } : {}), + } as OcxProviderConfig; +} + +function createRegisteredAdapter( + provider: OcxProviderConfig, + context: Parameters[1] = {}, +) { + return createProductionRegisteredAdapter( + provider, + provider.adapter === "mimo-free" + ? { ...context, mimoDeps: { getJwt: async () => "conformance-test-jwt" } } + : context, + ); +} + +function prepareWireParsed(parsed: OcxParsedRequest, wire: AdapterWire): OcxParsedRequest { + if (wire === "kiro") parsed._kiroAuthContext = { apiRegion: "us-east-1" }; + return parsed; +} + +function codeModeParsed(wire: AdapterWire): OcxParsedRequest { + const model = WIRE_MODELS[wire]; + return prepareWireParsed({ + modelId: model, + stream: true, + options: {}, + context: { + systemPrompt: ["Use apply_patch for local file edits."], + messages: [{ role: "user", content: "Patch the requested file.", timestamp: 0 }], + tools: [ + { name: "exec", description: execDescription, parameters: {} }, + { name: "wait", description: "Wait for work.", parameters: {} }, + ], + }, + _rawBody: { + model, + input: "Patch the requested file.", + stream: true, + tools: [{ + type: "custom", + name: "exec", + description: execDescription, + format: { type: "grammar", syntax: "lark" }, + }], + }, + }, wire); +} + +function freeformParsed(wire: AdapterWire): OcxParsedRequest { + return prepareWireParsed(parseRequest({ + model: WIRE_MODELS[wire], + input: "Apply the exact patch.", + stream: true, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }), wire); +} + +function toolChoiceNoneParsed(wire: AdapterWire): OcxParsedRequest { + return prepareWireParsed(parseRequest({ + model: WIRE_MODELS[wire], + input: "Do not call a tool.", + stream: true, + tool_choice: "none", + tools: [ + { type: "custom", name: "apply_patch", description: "Apply a patch" }, + { + type: "function", + name: "noop", + description: "No operation", + parameters: { type: "object", properties: {}, additionalProperties: false }, + }, + ], + }), wire); +} + +function continuationParsed(wire: AdapterWire): OcxParsedRequest { + return prepareWireParsed(parseRequest({ + model: WIRE_MODELS[wire], + input: [ + { + type: "custom_tool_call", + id: "ctc_patch", + call_id: "call_continue_patch", + name: "apply_patch", + input: APPLY_PATCH_FIXTURE, + }, + { + type: "custom_tool_call_output", + call_id: "call_continue_patch", + output: "Done!", + }, + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "Continue after patch." }], + }, + ], + stream: true, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }), wire); +} + +function parseResponsesFrames(text: string): Array<{ event?: string; data: Record }> { + return text.split("\n\n") + .map(frame => frame.trim()) + .filter(frame => frame.length > 0 && frame !== "data: [DONE]") + .map(frame => { + const lines = frame.split("\n"); + const event = lines.find(line => line.startsWith("event: "))?.slice(7); + const data = lines.find(line => line.startsWith("data: "))?.slice(6) ?? "{}"; + return { event, data: JSON.parse(data) as Record }; + }); +} + +function inputFromValue(value: unknown): string | undefined { + if (typeof value === "string") { + try { + const row = JSON.parse(value) as { input?: unknown }; + return typeof row.input === "string" ? row.input : value; + } catch { + return value; + } + } + if (value && typeof value === "object" && !Array.isArray(value)) { + const input = (value as Record).input; + if (typeof input === "string") return input; + } + return undefined; +} + +function advertisedToolNames(wire: AdapterWire, body: string): string[] { + const parsed = JSON.parse(body) as Record; + if (wire === "openai-chat") { + const tools = parsed.tools as Array<{ function?: { name?: string } }> | undefined; + return (tools ?? []).flatMap(tool => typeof tool.function?.name === "string" ? [tool.function.name] : []); + } + if (wire === "anthropic" || wire === "openai-responses" || wire === "cursor") { + const tools = parsed.tools as Array<{ name?: string }> | undefined; + return (tools ?? []).flatMap(tool => typeof tool.name === "string" ? [tool.name] : []); + } + if (wire === "google") { + const tools = parsed.tools as Array<{ functionDeclarations?: Array<{ name?: string }> }> | undefined; + return (tools ?? []).flatMap(group => + (group.functionDeclarations ?? []).flatMap(tool => typeof tool.name === "string" ? [tool.name] : [])); + } + if (wire === "command-code") { + const params = parsed.params as { tools?: Array<{ name?: string }> } | undefined; + return (params?.tools ?? []).flatMap(tool => typeof tool.name === "string" ? [tool.name] : []); + } + const state = parsed.conversationState as { + currentMessage?: { + userInputMessage?: { + userInputMessageContext?: { + tools?: Array<{ toolSpecification?: { name?: string } }>; + }; + }; + }; + } | undefined; + const tools = state?.currentMessage?.userInputMessage?.userInputMessageContext?.tools ?? []; + return tools.flatMap(tool => typeof tool.toolSpecification?.name === "string" ? [tool.toolSpecification.name] : []); +} + +function toolChoiceNoneDisablesCalls(wire: AdapterWire, body: string): boolean { + if (advertisedToolNames(wire, body).length === 0) return true; + const parsed = JSON.parse(body) as Record; + if (wire === "openai-chat" || wire === "openai-responses") { + return parsed.tool_choice === "none"; + } + if (wire === "anthropic") { + const choice = parsed.tool_choice as { type?: unknown } | undefined; + return choice?.type === "none"; + } + if (wire === "google") { + const config = parsed.toolConfig as { functionCallingConfig?: { mode?: unknown } } | undefined; + return config?.functionCallingConfig?.mode === "NONE"; + } + return false; +} + +function continuationInput(wire: AdapterWire, body: string): string | undefined { + const parsed = JSON.parse(body) as Record; + if (wire === "openai-chat") { + const messages = parsed.messages as Array<{ + tool_calls?: Array<{ function?: { name?: string; arguments?: unknown } }>; + }> | undefined; + for (const message of messages ?? []) { + for (const call of message.tool_calls ?? []) { + if (call.function?.name?.includes("apply_patch")) return inputFromValue(call.function.arguments); + } + } + return undefined; + } + if (wire === "anthropic") { + const messages = parsed.messages as Array<{ content?: unknown }> | undefined; + for (const message of messages ?? []) { + if (!Array.isArray(message.content)) continue; + for (const block of message.content) { + if (!block || typeof block !== "object" || Array.isArray(block)) continue; + const row = block as Record; + if (row.type === "tool_use" && typeof row.name === "string" && row.name.includes("apply_patch")) { + return inputFromValue(row.input); + } + } + } + return undefined; + } + if (wire === "google") { + const contents = parsed.contents as Array<{ + parts?: Array<{ functionCall?: { name?: string; args?: unknown } }>; + }> | undefined; + for (const content of contents ?? []) { + for (const part of content.parts ?? []) { + if (part.functionCall?.name?.includes("apply_patch")) return inputFromValue(part.functionCall.args); + } + } + return undefined; + } + if (wire === "command-code") { + const params = parsed.params as { + messages?: Array<{ content?: Array> }>; + } | undefined; + for (const message of params?.messages ?? []) { + for (const part of message.content ?? []) { + if (part.type === "tool-call" && typeof part.toolName === "string" && part.toolName.includes("apply_patch")) { + return inputFromValue(part.input); + } + } + } + return undefined; + } + if (wire === "kiro") { + const state = parsed.conversationState as { + history?: Array<{ assistantResponseMessage?: { toolUses?: Array<{ name?: string; input?: unknown }> } }>; + currentMessage?: { assistantResponseMessage?: { toolUses?: Array<{ name?: string; input?: unknown }> } }; + } | undefined; + const entries = [...(state?.history ?? []), ...(state?.currentMessage ? [state.currentMessage] : [])]; + for (const entry of entries) { + for (const use of entry.assistantResponseMessage?.toolUses ?? []) { + if (use.name?.includes("apply_patch")) return inputFromValue(use.input); + } + } + return undefined; + } + if (wire === "openai-responses") { + const input = parsed.input as Array> | undefined; + for (const item of input ?? []) { + if (typeof item.name !== "string" || !item.name.includes("apply_patch")) continue; + if (item.type === "custom_tool_call") return inputFromValue(item.input); + if (item.type === "function_call") return inputFromValue(item.arguments); + } + return undefined; + } + const visit = (value: unknown): string | undefined => { + if (!value || typeof value !== "object") return undefined; + if (Array.isArray(value)) { + for (const item of value) { + const found = visit(item); + if (found !== undefined) return found; + } + return undefined; + } + const row = value as Record; + if (typeof row.name === "string" && row.name.includes("apply_patch")) { + const found = inputFromValue(row.input ?? row.arguments); + if (found !== undefined) return found; + } + for (const nested of Object.values(row)) { + const found = visit(nested); + if (found !== undefined) return found; + } + return undefined; + }; + return visit(parsed); +} + +async function restoredFreeformInput(adapterId: string, wire: AdapterWire): Promise { + const driver = TOOL_WIRE_DRIVERS[wire]; + if (!driver.streamingToolCall) return undefined; + const parsed = freeformParsed(wire); + const adapter = createRegisteredAdapter(providerFixture(adapterId, wire)); + const outbound = await driver.observeOutbound(adapter, parsed); + const wireToolName = driver.extractWireToolName?.(outbound, "apply_patch") ?? "apply_patch"; + const upstream = driver.streamingToolCall(wireToolName, JSON.stringify({ input: APPLY_PATCH_FIXTURE })); + const maps = buildToolBridgeMaps(parsed); + const bridged = bridgeToResponsesSSE( + adapter.parseStream(upstream, createTestTranslatorBudget()), + parsed.modelId, + maps.toolNsMap, + maps.freeformToolNames, + maps.toolSearchToolNames, + undefined, + 2_000, + { declaredToolNames: maps.declaredToolNames }, + ); + const frames = parseResponsesFrames(await new Response(bridged).text()); + return frames.find(frame => frame.event === "response.custom_tool_call_input.done")?.data.input as + | string + | undefined; +} + +function bufferedToolResponse(wire: AdapterWire, wireName: string): Response | undefined { + const args = { input: APPLY_PATCH_FIXTURE }; + if (wire === "openai-chat") { + return new Response(JSON.stringify({ + choices: [{ + message: { + role: "assistant", + tool_calls: [{ + id: "call_buffered_patch", + type: "function", + function: { name: wireName, arguments: JSON.stringify(args) }, + }], + }, + finish_reason: "tool_calls", + }], + })); + } + if (wire === "anthropic") { + return new Response(JSON.stringify({ + content: [{ type: "tool_use", id: "call_buffered_patch", name: wireName, input: args }], + stop_reason: "tool_use", + usage: { input_tokens: 1, output_tokens: 1 }, + })); + } + if (wire === "google") { + return new Response(JSON.stringify({ + candidates: [{ + content: { parts: [{ functionCall: { name: wireName, args } }] }, + finishReason: "STOP", + }], + usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }, + })); + } + return TOOL_WIRE_DRIVERS[wire].streamingToolCall?.(wireName, JSON.stringify(args)); +} + +async function restoredBufferedInput(adapterId: string, wire: AdapterWire): Promise { + const parsed = freeformParsed(wire); + const adapter = createRegisteredAdapter(providerFixture(adapterId, wire)); + if (!adapter.parseResponse) return undefined; + const driver = TOOL_WIRE_DRIVERS[wire]; + const outbound = await driver.observeOutbound(adapter, parsed); + const wireName = driver.extractWireToolName?.(outbound, "apply_patch") ?? "apply_patch"; + const response = bufferedToolResponse(wire, wireName); + if (!response) return undefined; + const events = await adapter.parseResponse(response, createTestTranslatorBudget()); + const maps = buildToolBridgeMaps(parsed); + const built = buildResponseJSON(events, parsed.modelId, { + toolNsMap: maps.toolNsMap, + declaredToolNames: maps.declaredToolNames, + freeformToolNames: maps.freeformToolNames, + toolSearchToolNames: maps.toolSearchToolNames, + }); + const output = built.output as Array>; + const call = output.find(item => item.type === "custom_tool_call" && item.name === "apply_patch"); + return typeof call?.input === "string" ? call.input : undefined; +} + +function writeExec(path: string, text: string) { + return create(ExecServerMessageSchema, { + id: 7, + execId: "apply-patch-conformance", + message: { + case: "writeArgs", + value: create(WriteArgsSchema, { path, fileText: text }), + }, + }); +} + +function decodeCursorExec(bytes: Uint8Array) { + const message = fromBinary(AgentClientMessageSchema, bytes); + expect(message.message.case).toBe("execClientMessage"); + if (message.message.case !== "execClientMessage") throw new Error("Expected execClientMessage"); + return message.message.value; +} + +test("global apply_patch precondition preserves explicit tool representation and defaults missing values", () => { + const routed = normalizeRoutedCatalogEntry({ + slug: "xai/grok-4.6", + tool_mode: "legacy", + apply_patch_tool_type: "function", + context_window: 128_000, + }); + expect(routed.tool_mode).toBe("code_mode_only"); + expect(routed.apply_patch_tool_type).toBe("function"); + + const routedDefault = normalizeRoutedCatalogEntry({ + slug: "xai/grok-4.6", + context_window: 128_000, + }); + expect(routedDefault.apply_patch_tool_type).toBe("freeform"); + + const native = ensureStrictCatalogFields({ + slug: "gpt-5.6-sol", + apply_patch_tool_type: "function", + context_window: 128_000, + }); + expect(native.apply_patch_tool_type).toBe("function"); +}); + +test("every registered adapter automatically inherits the mandatory routed tool contracts", () => { + expect(adapterDefinitions().length).toBeGreaterThan(0); + for (const [adapterId, definition] of adapterDefinitions()) { + expect(definition.requiredToolContracts, adapterId).toEqual(REQUIRED_ROUTED_TOOL_CONTRACTS); + } +}); + +test("wrapper inheritance is strict, acyclic, and resolves to a direct semantic contract", () => { + for (const [adapterId, definition] of adapterDefinitions()) { + if (definition.kind === "wrapper") { + expect(ADAPTER_REGISTRY[definition.extends as keyof typeof ADAPTER_REGISTRY]).toBeTruthy(); + expect("wire" in definition).toBe(false); + expect("mutation" in definition).toBe(false); + } + const effective = effectiveAdapterContract(adapterId); + expect(effective.kind).toBe("direct"); + expect(effective.requiredToolContracts).toEqual(REQUIRED_ROUTED_TOOL_CONTRACTS); + } +}); + +test("all configured adapter ids are members of the authoritative adapter registry", () => { + for (const provider of PROVIDER_REGISTRY) { + expect(ADAPTER_REGISTRY[provider.adapter as keyof typeof ADAPTER_REGISTRY], provider.id).toBeTruthy(); + for (const value of Object.values(provider.modelWireDefaults ?? {})) { + const wire = typeof value === "string" ? value : value.wire; + expect(ADAPTER_REGISTRY[wire as keyof typeof ADAPTER_REGISTRY], `${provider.id}:${wire}`).toBeTruthy(); + } + } + for (const adapterId of MODEL_ADAPTER_OVERRIDE_ALLOWED) { + expect(ADAPTER_REGISTRY[adapterId as keyof typeof ADAPTER_REGISTRY], adapterId).toBeTruthy(); + } +}); + +test("every registered adapter keeps the nested apply_patch helper in its final request", async () => { + for (const [adapterId] of adapterDefinitions()) { + const contract = effectiveAdapterContract(adapterId); + const parsed = codeModeParsed(contract.wire); + const adapter = createRegisteredAdapter(providerFixture(adapterId, contract.wire)); + const body = await TOOL_WIRE_DRIVERS[contract.wire].observeOutbound(adapter, parsed); + const normalized = body.replace(/\\n/g, " ").replace(/\s+/g, " "); + expect(applyPatchContractFailures({ finalAdvertisement: normalized }), adapterId).toEqual([]); + } +}); + +test("tool_choice:none removes every registered adapter's callable tool surface", async () => { + for (const [adapterId] of adapterDefinitions()) { + const contract = effectiveAdapterContract(adapterId); + const parsed = toolChoiceNoneParsed(contract.wire); + const adapter = createRegisteredAdapter(providerFixture(adapterId, contract.wire)); + const body = await TOOL_WIRE_DRIVERS[contract.wire].observeOutbound(adapter, parsed); + const disabled = toolChoiceNoneDisablesCalls(contract.wire, body); + expect(disabled, adapterId).toBe(true); + expect(applyPatchContractFailures({ + expectedPatchAdvertised: false, + actualPatchAdvertised: !disabled, + }), adapterId).toEqual([]); + } +}); + +test("every parsed response wire restores the hostile freeform apply_patch input exactly", async () => { + for (const [adapterId] of adapterDefinitions()) { + const contract = effectiveAdapterContract(adapterId); + const driver = TOOL_WIRE_DRIVERS[contract.wire]; + if (!driver.streamingToolCall) { + // Responses passthrough and Cursor runTurn have focused byte-exact coverage. Any future + // wire without a streaming driver must add equally explicit coverage instead of being skipped. + expect(EXTERNAL_EXACT_ROUNDTRIP_COVERAGE.has(contract.wire), adapterId).toBe(true); + continue; + } + const restoredInput = await restoredFreeformInput(adapterId, contract.wire); + expect(restoredInput, adapterId).toBe(APPLY_PATCH_FIXTURE); + expect(applyPatchContractFailures({ restoredInput }), adapterId).toEqual([]); + } +}); + +test("every buffered parser restores the hostile freeform apply_patch input exactly", async () => { + for (const [adapterId] of adapterDefinitions()) { + const contract = effectiveAdapterContract(adapterId); + const adapter = createRegisteredAdapter(providerFixture(adapterId, contract.wire)); + if (!adapter.parseResponse) continue; + const restoredInput = await restoredBufferedInput(adapterId, contract.wire); + if (restoredInput === undefined && !bufferedToolResponse(contract.wire, "apply_patch")) continue; + expect(restoredInput, adapterId).toBe(APPLY_PATCH_FIXTURE); + } +}); + +test("every registered adapter replays the exact apply_patch input on continuation", async () => { + for (const [adapterId] of adapterDefinitions()) { + const contract = effectiveAdapterContract(adapterId); + const parsed = continuationParsed(contract.wire); + const adapter = createRegisteredAdapter(providerFixture(adapterId, contract.wire)); + const body = await TOOL_WIRE_DRIVERS[contract.wire].observeOutbound(adapter, parsed); + const replayed = continuationInput(contract.wire, body); + expect(replayed, adapterId).toBe(APPLY_PATCH_FIXTURE); + expect(applyPatchContractFailures({ continuationInput: replayed }), adapterId).toEqual([]); + } +}); + +test("Cursor mutation ownership follows the registry contract and the final tool catalog", async () => { + const contract = effectiveAdapterContract("cursor"); + expect(contract.mutation).toBe("mutation.codex-owned-with-gated-native-fallback"); + + const parsed: OcxParsedRequest = { + modelId: "cursor/auto", + context: { + messages: [{ role: "user", content: "Edit the requested file.", timestamp: 0 }], + tools: [ + { name: "exec", description: "Run JavaScript", parameters: {} }, + { name: "apply_patch", description: "Apply a Codex patch", parameters: {}, freeform: true }, + ], + }, + stream: false, + options: {}, + }; + + const advertised = createCursorRequest(parsed); + const rejectNativeFileMutations = cursorRequestAdvertisesApplyPatch(advertised.tools, advertised.toolChoice); + const structuredEditAvailable = advertised.tools?.some(isCursorSyntheticStructuredEditTool) ?? false; + expect(rejectNativeFileMutations).toBe(true); + + const blockedDir = mkdtempSync(join(tmpdir(), "ocx-cursor-cplus-blocked-")); + const allowedDir = mkdtempSync(join(tmpdir(), "ocx-cursor-cplus-allowed-")); + try { + const blockedPath = join(blockedDir, "blocked.txt"); + const blocked = decodeCursorExec((await handleCursorNativeExec(writeExec(blockedPath, "blocked"), { + unsafeAllowNativeLocalExec: true, + rejectNativeFileMutations, + structuredEditAvailable, + }))[0]!); + expect(blocked.message.case).toBe("writeResult"); + expect(blocked.message.value.result.case).toBe("rejected"); + expect(existsSync(blockedPath)).toBe(false); + + const fallback = createCursorRequest({ ...parsed, options: { toolChoice: { name: "exec" } } }); + const fallbackRejects = cursorRequestAdvertisesApplyPatch(fallback.tools, fallback.toolChoice); + expect(fallback.tools?.map(tool => tool.name)).toEqual(["exec"]); + expect(fallbackRejects).toBe(false); + + const allowedPath = join(allowedDir, "allowed.txt"); + const allowed = decodeCursorExec((await handleCursorNativeExec(writeExec(allowedPath, "native fallback"), { + unsafeAllowNativeLocalExec: true, + rejectNativeFileMutations: fallbackRejects, + }))[0]!); + expect(allowed.message.case).toBe("writeResult"); + expect(allowed.message.value.result.case).toBe("success"); + expect(readFileSync(allowedPath, "utf8")).toBe("native fallback"); + } finally { + rmSync(blockedDir, { recursive: true, force: true }); + rmSync(allowedDir, { recursive: true, force: true }); + } +}); + +test("the conformance oracle catches representative broken implementations", () => { + const truncated = APPLY_PATCH_FIXTURE.slice(0, -1); + const cases: Array<{ + name: string; + observation: ApplyPatchObservation; + contract: ApplyPatchContractId; + }> = [ + { + name: "drops patch declaration", + observation: { finalAdvertisement: "declare const tools: {};" }, + contract: "tools.code-mode-nested-helper", + }, + { + name: "forbids advertised patch helper", + observation: { finalAdvertisement: `${execDescription} Never use apply_patch.` }, + contract: "tools.code-mode-nested-helper", + }, + { + name: "truncates freeform input", + observation: { restoredInput: truncated }, + contract: "tools.freeform-exact-roundtrip", + }, + { + name: "ignores tool choice filtering", + observation: { expectedPatchAdvertised: false, actualPatchAdvertised: true }, + contract: "tools.tool-choice-final-catalog", + }, + { + name: "drops continuation input", + observation: { continuationInput: truncated }, + contract: "tools.continuation-replay", + }, + { + name: "allows alternate mutation while Codex owns it", + observation: { codexOwnsMutation: true, alternateMutationAllowed: true }, + contract: "mutation.codex-owned", + }, + ]; + for (const fault of cases) { + expect(applyPatchContractFailures(fault.observation), fault.name).toContain(fault.contract); + } +}); + +test("production and conformance routing cannot bypass the adapter registry", async () => { + const resolver = await readFile(adapterResolvePath, "utf8"); + expect(resolver).toContain("createRegisteredAdapter"); + expect(resolver).not.toMatch(/create(?:Anthropic|Azure|Cursor|Google|Kiro|MimoFree|OpenAIChat|CommandCode|ResponsesPassthrough)Adapter/); + + const files = [...await collectTypeScriptFiles(serverDir), routerPath, labExecutorPath]; + for (const path of files) { + expect(await directAdapterFactoryImports(path), path).toEqual([]); + } +}); diff --git a/tests/apply-patch-cursor-mutation-policy.test.ts b/tests/apply-patch-cursor-mutation-policy.test.ts new file mode 100644 index 000000000..889b21462 --- /dev/null +++ b/tests/apply-patch-cursor-mutation-policy.test.ts @@ -0,0 +1,106 @@ +import { existsSync, mkdtempSync, readFileSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { create, fromBinary } from "@bufbuild/protobuf"; +import { expect, test } from "bun:test"; +import { handleCursorNativeExec } from "../src/adapters/cursor/native-exec"; +import { createCursorRequest } from "../src/adapters/cursor/request-builder"; +import { + cursorRequestAdvertisesApplyPatch, + isCursorSyntheticStructuredEditTool, +} from "../src/adapters/cursor/tool-definitions"; +import { + AgentClientMessageSchema, + ExecServerMessageSchema, + WriteArgsSchema, +} from "../src/adapters/cursor/gen/agent_pb"; +import type { OcxParsedRequest, OcxToolChoice } from "../src/types"; + +const liveTransportPath = fileURLToPath(new URL("../src/adapters/cursor/live-transport.ts", import.meta.url)); + +function parsedRequest(toolChoice?: OcxToolChoice): OcxParsedRequest { + return { + modelId: "cursor/auto", + context: { + messages: [{ role: "user", content: "Edit the requested file.", timestamp: 0 }], + tools: [ + { name: "exec", description: "Run JavaScript", parameters: {} }, + { name: "apply_patch", description: "Apply a Codex patch", parameters: {}, freeform: true }, + ], + }, + stream: false, + options: toolChoice ? { toolChoice } : {}, + }; +} + +function writeExec(path: string, text: string) { + return create(ExecServerMessageSchema, { + id: 7, + execId: "apply-patch-policy", + message: { + case: "writeArgs", + value: create(WriteArgsSchema, { path, fileText: text }), + }, + }); +} + +function decode(bytes: Uint8Array) { + const message = fromBinary(AgentClientMessageSchema, bytes); + expect(message.message.case).toBe("execClientMessage"); + if (message.message.case !== "execClientMessage") throw new Error("Expected execClientMessage"); + return message.message.value; +} + +test("Cursor live transport wires native mutation rejection to the final apply_patch catalog", async () => { + const source = await readFile(liveTransportPath, "utf8"); + + expect(source).toMatch( + /rejectNativeFileMutations:\s*cursorRequestAdvertisesApplyPatch\(\s*request\.tools,\s*request\.toolChoice\s*\)/, + ); +}); + +test("Cursor rejects native writes when the final request advertises freeform apply_patch", async () => { + const request = createCursorRequest(parsedRequest()); + const rejectNativeFileMutations = cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice); + const structuredEditAvailable = request.tools?.some(isCursorSyntheticStructuredEditTool) ?? false; + + expect(request.tools?.some(tool => tool.name === "apply_patch" && tool.freeform === true)).toBe(true); + expect(rejectNativeFileMutations).toBe(true); + + const dir = mkdtempSync(join(tmpdir(), "ocx-cursor-patch-policy-")); + const path = join(dir, "blocked.txt"); + const result = decode((await handleCursorNativeExec(writeExec(path, "must not be written"), { + unsafeAllowNativeLocalExec: true, + rejectNativeFileMutations, + structuredEditAvailable, + }))[0]!); + + expect(result.message.case).toBe("writeResult"); + expect(result.message.value.result.case).toBe("rejected"); + if (result.message.value.result.case === "rejected") { + expect(result.message.value.result.value.reason).toContain("apply_patch"); + expect(result.message.value.result.value.reason).toContain("No file was changed."); + } + expect(existsSync(path)).toBe(false); +}); + +test("Cursor leaves native write fallback available when tool_choice removes apply_patch", async () => { + const request = createCursorRequest(parsedRequest({ name: "exec" })); + const rejectNativeFileMutations = cursorRequestAdvertisesApplyPatch(request.tools, request.toolChoice); + + expect(request.tools?.map(tool => tool.name)).toEqual(["exec"]); + expect(rejectNativeFileMutations).toBe(false); + + const dir = mkdtempSync(join(tmpdir(), "ocx-cursor-native-fallback-")); + const path = join(dir, "allowed.txt"); + const result = decode((await handleCursorNativeExec(writeExec(path, "native fallback"), { + unsafeAllowNativeLocalExec: true, + rejectNativeFileMutations, + }))[0]!); + + expect(result.message.case).toBe("writeResult"); + expect(result.message.value.result.case).toBe("success"); + expect(readFileSync(path, "utf8")).toBe("native fallback"); +}); diff --git a/tests/apply-patch-cursor-registry-runturn.test.ts b/tests/apply-patch-cursor-registry-runturn.test.ts new file mode 100644 index 000000000..06d4a256e --- /dev/null +++ b/tests/apply-patch-cursor-registry-runturn.test.ts @@ -0,0 +1,102 @@ +import { expect, test } from "bun:test"; +import { createRegisteredAdapter } from "../src/adapters/registry"; +import { + createDisabledCursorTransport, + type CursorTransport, +} from "../src/adapters/cursor/transport"; +import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { createTestTranslatorBudget } from "./helpers/translator-budget"; + +const provider: OcxProviderConfig = { + adapter: "cursor", + baseUrl: "https://api2.cursor.sh", +}; + +const parsed: OcxParsedRequest = { + modelId: "cursor/auto", + context: { + messages: [{ role: "user", content: "Edit the file.", timestamp: 0 }], + tools: [{ + name: "exec", + description: "Run JavaScript. declare const tools: { apply_patch(input: string): Promise; };", + parameters: {}, + }], + }, + stream: true, + options: {}, +}; + +const PATCH_INPUT = [ + "*** Begin Patch", + "*** Update File: src/example.ts", + "@@", + '-const value = "old";', + '+const value = "new \\"quoted\\" \\\\ path 🧪";', + "*** End Patch", +].join("\n"); + +function createApplyPatchCursorTransport(): CursorTransport { + return { + async *run() { + const split = Math.floor(PATCH_INPUT.length / 2); + yield { type: "tool_call_start", id: "call_patch", name: "apply_patch" }; + yield { type: "tool_call_delta", arguments: PATCH_INPUT.slice(0, split) }; + yield { type: "tool_call_delta", arguments: PATCH_INPUT.slice(split) }; + yield { type: "tool_call_end", id: "call_patch" }; + yield { type: "done" }; + }, + writeClient() {}, + close() {}, + requestCommitted() { + return true; + }, + }; +} + +test("Cursor registry construction reaches the real runTurn path with injected transport deps", async () => { + const adapter = createRegisteredAdapter(provider, { + cursorDeps: { createTransport: createDisabledCursorTransport }, + }); + const events: AdapterEvent[] = []; + + await adapter.runTurn?.( + parsed, + { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, + event => events.push(event), + ); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "error", + message: expect.stringContaining("explicit disabled Cursor transport was injected"), + }); +}); + +test("Cursor registry runTurn preserves exact freeform apply_patch input", async () => { + const adapter = createRegisteredAdapter(provider, { + cursorDeps: { createTransport: () => createApplyPatchCursorTransport() }, + }); + const events: AdapterEvent[] = []; + + await adapter.runTurn?.( + structuredClone(parsed), + { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, + event => events.push(event), + ); + + expect(events.find(event => event.type === "tool_call_start")).toMatchObject({ + type: "tool_call_start", + id: "call_patch", + name: "apply_patch", + }); + + const restored = events + .filter((event): event is Extract => + event.type === "tool_call_delta") + .map(event => event.arguments) + .join(""); + + expect(restored).toBe(PATCH_INPUT); + expect(events.some(event => event.type === "tool_call_end")).toBe(true); + expect(events.at(-1)?.type).toBe("done"); +}); diff --git a/tests/apply-patch-fragmentation.test.ts b/tests/apply-patch-fragmentation.test.ts new file mode 100644 index 000000000..58acab428 --- /dev/null +++ b/tests/apply-patch-fragmentation.test.ts @@ -0,0 +1,126 @@ +import { expect, test } from "bun:test"; +import { bridgeToResponsesSSE } from "../src/bridge"; +import { createRegisteredAdapter } from "../src/adapters/registry"; +import type { AdapterWire } from "../src/adapters/contracts"; +import { parseRequest } from "../src/responses/parser"; +import { buildToolBridgeMaps } from "../src/server/responses"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { APPLY_PATCH_FIXTURE } from "./helpers/apply-patch-conformance/contracts"; +import { TOOL_WIRE_DRIVERS } from "./helpers/apply-patch-conformance/wire-drivers"; +import { createTestTranslatorBudget } from "./helpers/translator-budget"; + +const FRAGMENTABLE_WIRES = ["openai-chat", "anthropic", "kiro"] as const satisfies readonly AdapterWire[]; + +const WIRE_MODELS: Record<(typeof FRAGMENTABLE_WIRES)[number], string> = { + "openai-chat": "grok-4.6", + anthropic: "claude-haiku-4-5", + kiro: "claude-sonnet-4.5", +}; + +function providerFixture(wire: (typeof FRAGMENTABLE_WIRES)[number]): OcxProviderConfig { + if (wire === "openai-chat") { + return { + adapter: wire, + baseUrl: "https://api.x.ai/v1", + authMode: "key", + apiKey: "test-key", + defaultMaxOutputTokens: 64_000, + } as OcxProviderConfig; + } + if (wire === "anthropic") { + return { + adapter: wire, + baseUrl: "https://api.anthropic.com", + authMode: "oauth", + apiKey: "test-key", + defaultMaxOutputTokens: 64_000, + } as OcxProviderConfig; + } + return { + adapter: wire, + baseUrl: "https://runtime.us-east-1.kiro.dev", + authMode: "key", + apiKey: "ksk_test", + defaultMaxOutputTokens: 64_000, + } as OcxProviderConfig; +} + +function freeformParsed(wire: (typeof FRAGMENTABLE_WIRES)[number]): OcxParsedRequest { + const parsed = parseRequest({ + model: WIRE_MODELS[wire], + input: "Apply the exact patch.", + stream: true, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }); + if (wire === "kiro") parsed._kiroAuthContext = { apiRegion: "us-east-1" }; + return parsed; +} + +function parseResponsesFrames(text: string): Array<{ event?: string; data: Record }> { + return text.split("\n\n") + .map(frame => frame.trim()) + .filter(frame => frame.length > 0 && frame !== "data: [DONE]") + .map(frame => { + const lines = frame.split("\n"); + const event = lines.find(line => line.startsWith("event: "))?.slice(7); + const data = lines.find(line => line.startsWith("data: "))?.slice(6) ?? "{}"; + return { event, data: JSON.parse(data) as Record }; + }); +} + +async function restoredInput( + wire: (typeof FRAGMENTABLE_WIRES)[number], + fragments: readonly string[], +): Promise { + const driver = TOOL_WIRE_DRIVERS[wire]; + if (!driver.streamingToolCallFragments) { + throw new Error(`${wire} must expose fragmentable streaming fixtures`); + } + + const parsed = freeformParsed(wire); + const adapter = createRegisteredAdapter(providerFixture(wire)); + const outbound = await driver.observeOutbound(adapter, parsed); + const wireToolName = driver.extractWireToolName?.(outbound, "apply_patch") ?? "apply_patch"; + const upstream = driver.streamingToolCallFragments(wireToolName, fragments); + const maps = buildToolBridgeMaps(parsed); + const bridged = bridgeToResponsesSSE( + adapter.parseStream(upstream, createTestTranslatorBudget()), + parsed.modelId, + maps.toolNsMap, + maps.freeformToolNames, + maps.toolSearchToolNames, + undefined, + 2_000, + { declaredToolNames: maps.declaredToolNames }, + ); + const frames = parseResponsesFrames(await new Response(bridged).text()); + return frames.find(frame => frame.event === "response.custom_tool_call_input.done")?.data.input as + | string + | undefined; +} + +function fixedWidthFragments(input: string, width: number): string[] { + const fragments: string[] = []; + for (let index = 0; index < input.length; index += width) { + fragments.push(input.slice(index, index + width)); + } + return fragments; +} + +for (const wire of FRAGMENTABLE_WIRES) { + test(`${wire} restores apply_patch across every two-way argument split`, async () => { + const encoded = JSON.stringify({ input: APPLY_PATCH_FIXTURE }); + for (let split = 1; split < encoded.length; split += 1) { + const actual = await restoredInput(wire, [encoded.slice(0, split), encoded.slice(split)]); + expect(actual, `${wire} split ${split}/${encoded.length}`).toBe(APPLY_PATCH_FIXTURE); + } + }); + + test(`${wire} restores apply_patch across repeated small argument fragments`, async () => { + const encoded = JSON.stringify({ input: APPLY_PATCH_FIXTURE }); + for (const width of [1, 2, 3, 7, 13]) { + const actual = await restoredInput(wire, fixedWidthFragments(encoded, width)); + expect(actual, `${wire} fragment width ${width}`).toBe(APPLY_PATCH_FIXTURE); + } + }); +} diff --git a/tests/apply-patch-responses-native-contract.test.ts b/tests/apply-patch-responses-native-contract.test.ts new file mode 100644 index 000000000..ec021e372 --- /dev/null +++ b/tests/apply-patch-responses-native-contract.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from "bun:test"; +import { createResponsesPassthroughAdapter } from "../src/adapters/openai-responses"; +import type { OcxParsedRequest, OcxProviderConfig } from "../src/types"; +import { withTestTranslatorBudget } from "./helpers/translator-budget"; + +const execDescription = + "Run JavaScript. declare const tools: { apply_patch(input: string): Promise; };"; + +test("routed Responses conversion preserves the nested Code Mode apply_patch declaration", () => { + const rawBody = { + model: "deepseek-v4-flash", + input: "Patch a file.", + tools: [{ + type: "custom", + name: "exec", + description: execDescription, + format: { type: "grammar", syntax: "lark" }, + }], + }; + const parsed = { + modelId: rawBody.model, + stream: true, + options: {}, + context: { messages: [] }, + _rawBody: rawBody, + } as OcxParsedRequest; + const adapter = withTestTranslatorBudget(createResponsesPassthroughAdapter({ + adapter: "openai-responses", + baseUrl: "https://api.deepseek.com", + responsesPath: "/responses", + authMode: "key", + apiKey: "test-key", + } as OcxProviderConfig)); + + const request = adapter.buildRequest(parsed); + const body = JSON.parse(request.body) as { + tools: Array>; + }; + + expect(body.tools).toHaveLength(1); + expect(body.tools[0]).toMatchObject({ + type: "function", + name: "exec", + description: execDescription, + }); + expect(JSON.stringify(body.tools[0])).toContain("apply_patch"); + request.releaseBodyObservation?.(); +}); diff --git a/tests/apply-patch-routed-safety-net.test.ts b/tests/apply-patch-routed-safety-net.test.ts new file mode 100644 index 000000000..92c3be026 --- /dev/null +++ b/tests/apply-patch-routed-safety-net.test.ts @@ -0,0 +1,285 @@ +import { expect, test } from "bun:test"; +import { createOpenAIChatAdapter as createOpenAIChatAdapterProduction } from "../src/adapters/openai-chat"; +import { bridgeToResponsesSSE, buildResponseJSON } from "../src/bridge"; +import { parseRequest } from "../src/responses/parser"; +import { buildToolBridgeMaps } from "../src/server/responses"; +import type { OcxProviderConfig } from "../src/types"; +import { withTestTranslatorBudget } from "./helpers/translator-budget"; + +const provider: OcxProviderConfig = { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + apiKey: "test-key", +}; + +const createOpenAIChatAdapter = (...args: Parameters) => + withTestTranslatorBudget(createOpenAIChatAdapterProduction(...args)); + +function parseSse(text: string): Array<{ event?: string; data: Record }> { + return text.split("\n\n") + .map(frame => frame.trim()) + .filter(frame => frame.length > 0 && frame !== "data: [DONE]") + .map(frame => { + const lines = frame.split("\n"); + const event = lines.find(line => line.startsWith("event: "))?.slice(7); + const data = lines.find(line => line.startsWith("data: "))?.slice(6) ?? "{}"; + return { event, data: JSON.parse(data) as Record }; + }); +} + +test("Code Mode exec round-trips JavaScript that invokes nested tools.apply_patch", async () => { + const patch = [ + "*** Begin Patch", + "*** Add File: code-mode-patch.txt", + "+from code mode", + "*** End Patch", + ].join("\n"); + const source = [ + `const patch = ${JSON.stringify(patch)};`, + "await tools.apply_patch(patch);", + ].join("\n"); + const parsed = parseRequest({ + model: "xai/grok-4.6", + input: "Apply the patch through Code Mode.", + stream: true, + tools: [{ type: "custom", name: "exec", description: "Run JavaScript" }], + }); + const maps = buildToolBridgeMaps(parsed); + expect(maps.freeformToolNames.has("exec")).toBe(true); + + const adapter = createOpenAIChatAdapter(provider); + const upstreamPayload = JSON.stringify({ + choices: [{ + delta: { + tool_calls: [{ + index: 0, + id: "call_exec", + type: "function", + function: { + name: "exec", + arguments: JSON.stringify({ input: source }), + }, + }], + }, + finish_reason: "tool_calls", + }], + }); + const bridged = bridgeToResponsesSSE( + adapter.parseStream(new Response(`data: ${upstreamPayload}\n\ndata: [DONE]\n\n`)), + parsed.modelId, + maps.toolNsMap, + maps.freeformToolNames, + maps.toolSearchToolNames, + undefined, + 2_000, + { declaredToolNames: maps.declaredToolNames }, + ); + const frames = parseSse(await new Response(bridged).text()); + const execDone = frames.find(frame => { + if (frame.event !== "response.output_item.done") return false; + const item = frame.data.item as Record | undefined; + return item?.type === "custom_tool_call" && item.name === "exec"; + })?.data.item as Record | undefined; + + expect(execDone).toMatchObject({ + type: "custom_tool_call", + call_id: "call_exec", + name: "exec", + input: source, + status: "completed", + }); + expect(execDone?.input).toContain("tools.apply_patch"); + expect(execDone?.input).toContain(JSON.stringify(patch)); + expect(frames.some(frame => frame.event === "response.failed")).toBe(false); +}); + +test("fragmented streamed apply_patch arguments survive JSON escape boundaries", async () => { + const patch = [ + "*** Begin Patch", + "*** Add File: fragmented-patch.txt", + "+fragmented", + "*** End Patch", + ].join("\n"); + const parsed = parseRequest({ + model: "xai/grok-4.6", + input: "Create the fragmented smoke-test file.", + stream: true, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }); + const maps = buildToolBridgeMaps(parsed); + const adapter = createOpenAIChatAdapter(provider); + const encodedArgs = JSON.stringify({ input: patch }); + const slashPositions = [...encodedArgs.matchAll(/\\n/g)].map(match => match.index!); + const cuts = [10, ...slashPositions.flatMap(index => [index + 1, index + 2]), encodedArgs.length] + .filter((value, index, all) => value > 0 && value <= encodedArgs.length && all.indexOf(value) === index) + .sort((a, b) => a - b); + let start = 0; + const fragments = cuts.map(end => { + const fragment = encodedArgs.slice(start, end); + start = end; + return fragment; + }).filter(fragment => fragment.length > 0); + + const wire = fragments.map((argumentsFragment, index) => { + const first = index === 0; + const last = index === fragments.length - 1; + return `data: ${JSON.stringify({ + choices: [{ + delta: { + tool_calls: [{ + index: 0, + ...(first ? { id: "call_fragmented", type: "function" } : {}), + function: { + ...(first ? { name: "apply_patch" } : {}), + arguments: argumentsFragment, + }, + }], + }, + ...(last ? { finish_reason: "tool_calls" } : {}), + }], + })}\n\n`; + }).join("") + "data: [DONE]\n\n"; + + const bridged = bridgeToResponsesSSE( + adapter.parseStream(new Response(wire)), + parsed.modelId, + maps.toolNsMap, + maps.freeformToolNames, + maps.toolSearchToolNames, + undefined, + 2_000, + { declaredToolNames: maps.declaredToolNames }, + ); + const frames = parseSse(await new Response(bridged).text()); + const streamedInput = frames + .filter(frame => frame.event === "response.custom_tool_call_input.delta") + .map(frame => frame.data.delta as string) + .join(""); + const inputDone = frames.find(frame => frame.event === "response.custom_tool_call_input.done")?.data; + + expect(slashPositions.length).toBeGreaterThan(0); + expect(streamedInput).toBe(patch); + expect(inputDone?.input).toBe(patch); + expect(frames.some(frame => frame.event === "response.failed")).toBe(false); +}); + +test("non-streaming routed apply_patch restores a custom_tool_call with exact input", async () => { + const patch = [ + "*** Begin Patch", + "*** Add File: non-streaming-patch.txt", + "+non streaming", + "*** End Patch", + ].join("\n"); + const parsed = parseRequest({ + model: "xai/grok-4.6", + input: "Create the non-streaming smoke-test file.", + stream: false, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }); + const maps = buildToolBridgeMaps(parsed); + const adapter = createOpenAIChatAdapter(provider); + expect(adapter.parseResponse).toBeDefined(); + + const events = await adapter.parseResponse!(new Response(JSON.stringify({ + choices: [{ + message: { + role: "assistant", + tool_calls: [{ + id: "call_nonstream", + type: "function", + function: { + name: "apply_patch", + arguments: JSON.stringify({ input: patch }), + }, + }], + }, + finish_reason: "tool_calls", + }], + }))); + const response = buildResponseJSON(events, parsed.modelId, { + toolNsMap: maps.toolNsMap, + declaredToolNames: maps.declaredToolNames, + freeformToolNames: maps.freeformToolNames, + toolSearchToolNames: maps.toolSearchToolNames, + }); + const output = response.output as Array>; + + expect(response.status).toBe("completed"); + expect(output).toContainEqual(expect.objectContaining({ + type: "custom_tool_call", + call_id: "call_nonstream", + name: "apply_patch", + input: patch, + status: "completed", + })); +}); + +test("apply_patch call and result replay into the next routed chat request", () => { + const patch = [ + "*** Begin Patch", + "*** Add File: continuation-patch.txt", + "+continuation", + "*** End Patch", + ].join("\n"); + const parsed = parseRequest({ + model: "xai/grok-4.6", + input: [ + { + type: "custom_tool_call", + id: "ctc_patch", + call_id: "call_continue_patch", + name: "apply_patch", + input: patch, + }, + { + type: "custom_tool_call_output", + call_id: "call_continue_patch", + output: "Done!", + }, + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "Continue after patch." }], + }, + ], + stream: true, + tools: [{ type: "custom", name: "apply_patch", description: "Apply a patch" }], + }); + const adapter = createOpenAIChatAdapter(provider); + const outbound = JSON.parse(adapter.buildRequest(parsed).body) as { + messages: Array<{ + role?: string; + content?: unknown; + tool_call_id?: string; + tool_calls?: Array<{ + id?: string; + function?: { name?: string; arguments?: string }; + }>; + }>; + }; + + const callIndex = outbound.messages.findIndex(message => + message.role === "assistant" + && message.tool_calls?.some(call => call.id === "call_continue_patch"), + ); + const resultIndex = outbound.messages.findIndex(message => + message.role === "tool" && message.tool_call_id === "call_continue_patch", + ); + const userIndex = outbound.messages.findIndex(message => + message.role === "user" && message.content === "Continue after patch.", + ); + + expect(callIndex).toBeGreaterThanOrEqual(0); + expect(resultIndex).toBeGreaterThan(callIndex); + expect(userIndex).toBeGreaterThan(resultIndex); + + const replayedCall = outbound.messages[callIndex]?.tool_calls?.find(call => call.id === "call_continue_patch"); + expect(replayedCall?.function?.name).toBe("apply_patch"); + expect(JSON.parse(replayedCall?.function?.arguments ?? "{}")) + .toEqual({ input: patch }); + expect(outbound.messages[resultIndex]).toMatchObject({ + role: "tool", + tool_call_id: "call_continue_patch", + content: "Done!", + }); +}); diff --git a/tests/helpers/apply-patch-conformance/contracts.ts b/tests/helpers/apply-patch-conformance/contracts.ts new file mode 100644 index 000000000..8ed05a058 --- /dev/null +++ b/tests/helpers/apply-patch-conformance/contracts.ts @@ -0,0 +1,54 @@ +export const APPLY_PATCH_FIXTURE = [ + "*** Begin Patch", + "*** Update File: a \\\"quoted\\\" path.txt", + "@@", + "-old\\\\value", + "+new\\\\value", + "+unicode: Ω 漢字 🚀", + "+json-ish: {\\\"x\\\":\\\"\\\\\\\\n\\\",\\\"quote\\\":\\\"\\\\\\\"\\\"}", + "*** End Patch", +].join("\n"); + +export type ApplyPatchContractId = + | "tools.code-mode-nested-helper" + | "tools.freeform-exact-roundtrip" + | "tools.tool-choice-final-catalog" + | "tools.continuation-replay" + | "mutation.codex-owned"; + +export interface ApplyPatchObservation { + finalAdvertisement?: string; + restoredInput?: string; + expectedPatchAdvertised?: boolean; + actualPatchAdvertised?: boolean; + continuationInput?: string; + codexOwnsMutation?: boolean; + alternateMutationAllowed?: boolean; +} + +const prohibition = /(?:do not|don't|never|must not|cannot|can't)[^.]{0,260}\bapply_patch\b|\bapply_patch\b[^.]{0,180}\b(?:forbidden|unavailable|off-limits)/i; + +export function applyPatchContractFailures(observation: ApplyPatchObservation): ApplyPatchContractId[] { + const failures: ApplyPatchContractId[] = []; + if (observation.finalAdvertisement !== undefined) { + if (!observation.finalAdvertisement.includes("apply_patch(input: string)") || prohibition.test(observation.finalAdvertisement)) { + failures.push("tools.code-mode-nested-helper"); + } + } + if (observation.restoredInput !== undefined && observation.restoredInput !== APPLY_PATCH_FIXTURE) { + failures.push("tools.freeform-exact-roundtrip"); + } + if ( + observation.expectedPatchAdvertised !== undefined + && observation.actualPatchAdvertised !== observation.expectedPatchAdvertised + ) { + failures.push("tools.tool-choice-final-catalog"); + } + if (observation.continuationInput !== undefined && observation.continuationInput !== APPLY_PATCH_FIXTURE) { + failures.push("tools.continuation-replay"); + } + if (observation.codexOwnsMutation === true && observation.alternateMutationAllowed === true) { + failures.push("mutation.codex-owned"); + } + return failures; +} diff --git a/tests/helpers/apply-patch-conformance/wire-drivers.ts b/tests/helpers/apply-patch-conformance/wire-drivers.ts new file mode 100644 index 000000000..bc6e71d35 --- /dev/null +++ b/tests/helpers/apply-patch-conformance/wire-drivers.ts @@ -0,0 +1,212 @@ +import type { ProviderAdapter } from "../../../src/adapters/base"; +import type { AdapterWire } from "../../../src/adapters/contracts"; +import { createCursorRequest } from "../../../src/adapters/cursor/request-builder"; +import { encodeMessage } from "../../../src/lib/eventstream-decoder"; +import type { OcxParsedRequest } from "../../../src/types"; +import { withTestTranslatorBudget } from "../translator-budget"; + +export interface ToolWireDriver { + observeOutbound(adapter: ProviderAdapter, parsed: OcxParsedRequest): Promise; + extractWireToolName?(body: string, canonicalName: string): string; + streamingToolCall?(wireName: string, wrappedArguments: string): Response; + streamingToolCallFragments?(wireName: string, argumentFragments: readonly string[]): Response; +} + +async function observeHttpOutbound(adapter: ProviderAdapter, parsed: OcxParsedRequest): Promise { + const testAdapter = withTestTranslatorBudget(adapter); + const request = await testAdapter.buildRequest(parsed); + try { + return request.body; + } finally { + request.releaseBodyObservation?.(); + } +} + +function splitInTwo(input: string): [string, string] { + const split = Math.max(1, Math.floor(input.length / 2)); + return [input.slice(0, split), input.slice(split)]; +} + +function openAiChatToolCallFragments(wireName: string, argumentFragments: readonly string[]): Response { + const fragments = argumentFragments.length > 0 ? argumentFragments : [""]; + const frames = fragments.map((argumentsFragment, index) => ({ + choices: [{ + delta: { + tool_calls: [{ + index: 0, + ...(index === 0 ? { id: "call_patch", type: "function" } : {}), + function: { + ...(index === 0 ? { name: wireName } : {}), + arguments: argumentsFragment, + }, + }], + }, + finish_reason: index === fragments.length - 1 ? "tool_calls" : null, + }], + })); + return new Response(`${frames.map(frame => `data: ${JSON.stringify(frame)}`).join("\n\n")}\n\ndata: [DONE]\n\n`, { + headers: { "content-type": "text/event-stream" }, + }); +} + +function openAiChatToolCall(wireName: string, wrappedArguments: string): Response { + return openAiChatToolCallFragments(wireName, splitInTwo(wrappedArguments)); +} + +function anthropicToolCallFragments(wireName: string, argumentFragments: readonly string[]): Response { + const frame = (event: string, data: unknown) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`; + return new Response([ + frame("content_block_start", { + type: "content_block_start", + content_block: { type: "tool_use", id: "toolu_patch", name: wireName }, + }), + ...argumentFragments.map(partialJson => frame("content_block_delta", { + type: "content_block_delta", + delta: { type: "input_json_delta", partial_json: partialJson }, + })), + frame("content_block_stop", { type: "content_block_stop" }), + frame("message_stop", { type: "message_stop" }), + ].join(""), { headers: { "content-type": "text/event-stream" } }); +} + +function anthropicToolCall(wireName: string, wrappedArguments: string): Response { + return anthropicToolCallFragments(wireName, splitInTwo(wrappedArguments)); +} + +function googleToolCall(wireName: string, wrappedArguments: string): Response { + return new Response( + `data: ${JSON.stringify({ + candidates: [{ + content: { parts: [{ functionCall: { name: wireName, args: JSON.parse(wrappedArguments) } }] }, + finishReason: "STOP", + }], + })}\n\n`, + { headers: { "content-type": "text/event-stream" } }, + ); +} + +function commandCodeToolCall(wireName: string, wrappedArguments: string): Response { + return new Response([ + JSON.stringify({ + type: "tool-call", + toolCallId: "call_patch", + toolName: wireName, + input: JSON.parse(wrappedArguments), + }), + JSON.stringify({ type: "finish", rawFinishReason: "tool_use" }), + ].join("\n")); +} + +const kiroEncoder = new TextEncoder(); +function kiroFrame(payload: unknown): Uint8Array { + return encodeMessage( + { ":message-type": "event", ":event-type": "toolUseEvent" }, + kiroEncoder.encode(JSON.stringify(payload)), + ); +} + +function kiroToolCallFragments(wireName: string, argumentFragments: readonly string[]): Response { + const frames = [ + kiroFrame({ name: wireName, toolUseId: "call_patch" }), + ...argumentFragments.map(input => kiroFrame({ input, name: wireName, toolUseId: "call_patch" })), + kiroFrame({ name: wireName, stop: true, toolUseId: "call_patch" }), + ]; + let index = 0; + return new Response(new ReadableStream({ + pull(controller) { + if (index < frames.length) controller.enqueue(frames[index++]!); + else controller.close(); + }, + })); +} + +function kiroToolCall(wireName: string, wrappedArguments: string): Response { + return kiroToolCallFragments(wireName, splitInTwo(wrappedArguments)); +} + +const openAiChatDriver: ToolWireDriver = { + observeOutbound: observeHttpOutbound, + extractWireToolName(body, canonicalName) { + const parsed = JSON.parse(body) as { tools?: Array<{ function?: { name?: string } }> }; + return parsed.tools?.find(tool => tool.function?.name?.includes(canonicalName))?.function?.name ?? canonicalName; + }, + streamingToolCall: openAiChatToolCall, + streamingToolCallFragments: openAiChatToolCallFragments, +}; + +const anthropicDriver: ToolWireDriver = { + observeOutbound: observeHttpOutbound, + extractWireToolName(body, canonicalName) { + const parsed = JSON.parse(body) as { tools?: Array<{ name?: string }> }; + return parsed.tools?.find(tool => tool.name?.includes(canonicalName))?.name ?? canonicalName; + }, + streamingToolCall: anthropicToolCall, + streamingToolCallFragments: anthropicToolCallFragments, +}; + +const googleDriver: ToolWireDriver = { + observeOutbound: observeHttpOutbound, + extractWireToolName(body, canonicalName) { + const parsed = JSON.parse(body) as { + tools?: Array<{ functionDeclarations?: Array<{ name?: string }> }>; + }; + for (const toolGroup of parsed.tools ?? []) { + const match = toolGroup.functionDeclarations?.find(tool => tool.name?.includes(canonicalName)); + if (match?.name) return match.name; + } + return canonicalName; + }, + streamingToolCall: googleToolCall, +}; + +const commandCodeDriver: ToolWireDriver = { + observeOutbound: observeHttpOutbound, + extractWireToolName(body, canonicalName) { + const parsed = JSON.parse(body) as { params?: { tools?: Array<{ name?: string }> } }; + return parsed.params?.tools?.find(tool => tool.name?.includes(canonicalName))?.name ?? canonicalName; + }, + streamingToolCall: commandCodeToolCall, +}; + +const kiroDriver: ToolWireDriver = { + observeOutbound: observeHttpOutbound, + extractWireToolName(body, canonicalName) { + const parsed = JSON.parse(body) as { + conversationState?: { + currentMessage?: { + userInputMessage?: { + userInputMessageContext?: { + tools?: Array<{ toolSpecification?: { name?: string } }>; + }; + }; + }; + }; + }; + const tools = parsed.conversationState?.currentMessage?.userInputMessage?.userInputMessageContext?.tools ?? []; + return tools.find(tool => tool.toolSpecification?.name?.includes(canonicalName))?.toolSpecification?.name ?? canonicalName; + }, + streamingToolCall: kiroToolCall, + streamingToolCallFragments: kiroToolCallFragments, +}; + +const responsesDriver: ToolWireDriver = { + observeOutbound: observeHttpOutbound, + extractWireToolName(body, canonicalName) { + const parsed = JSON.parse(body) as { tools?: Array<{ name?: string }> }; + return parsed.tools?.find(tool => tool.name?.includes(canonicalName))?.name ?? canonicalName; + }, +}; + +export const TOOL_WIRE_DRIVERS = { + "openai-chat": openAiChatDriver, + anthropic: anthropicDriver, + google: googleDriver, + "command-code": commandCodeDriver, + kiro: kiroDriver, + "openai-responses": responsesDriver, + cursor: { + async observeOutbound(_adapter, parsed) { + return JSON.stringify(createCursorRequest(parsed)); + }, + }, +} satisfies Record;