From 16106277fb7c3482711d5f3ed7b80f80d2fc3720 Mon Sep 17 00:00:00 2001 From: jatmn Date: Sat, 18 Jul 2026 13:26:18 -0700 Subject: [PATCH 01/25] refactor(openai-shim): extract request execution --- src/services/api/openaiShim.ts | 820 +-- .../api/openaiShim/architecture.test.ts | 35 + .../requestExecutor.integration.test.ts | 4709 +++++++++++++++++ .../api/openaiShim/requestExecutor.test.ts | 1913 +++++++ .../api/openaiShim/requestExecutor.ts | 1007 ++++ 5 files changed, 7670 insertions(+), 814 deletions(-) create mode 100644 src/services/api/openaiShim/architecture.test.ts create mode 100644 src/services/api/openaiShim/requestExecutor.integration.test.ts create mode 100644 src/services/api/openaiShim/requestExecutor.test.ts create mode 100644 src/services/api/openaiShim/requestExecutor.ts diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 8e981879d9..67cc4bbdb4 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -98,6 +98,7 @@ import { fetchWithProxyRetry, type ProxyRetryFetcher, } from './fetchWithProxyRetry.js' +import { executeOpenAIRequest } from './openaiShim/requestExecutor.js' import { getLocalFastPathConfig, getLocalProviderRetryBaseUrls, @@ -1463,821 +1464,12 @@ class OpenAIShimMessages { // Extraction boundary: request planning | request execution. // The prepared body builders above are executor inputs, not executor-owned logic. // Keep this marker stable so either extraction can merge independently. - const baseHeaders: Record = { - 'Content-Type': 'application/json', - ...filterAnthropicHeaders(shimConfig.headers), - ...this.defaultHeaders, - ...filterAnthropicHeaders(options?.headers), - } - - const isGemini = isGeminiMode() - const routeCredential = resolveRouteCredentialValue({ - routeId: runtimeShimContext.routeId, - baseUrl: request.baseUrl, - processEnv: process.env, - }) - // xAI OAuth: when the active route is xAI and no API key is set, fall - // back to a stored OAuth access token (auto-refreshed). The token is - // sent as a Bearer to api.x.ai/v1 — same surface as an API key. - const isXaiRoute = - runtimeShimContext.routeId === 'xai' || isXaiBaseUrl(request.baseUrl) - const routeAcceptsGenericOpenAICredentials = - runtimeShimContext.routeId === null || - getRouteDescriptor(runtimeShimContext.routeId)?.setup - .dedicatedCredentialsOnly !== true - const openAIApiKeysPoolRaw = - routeAcceptsGenericOpenAICredentials && - parseCredentialList(process.env.OPENAI_API_KEYS).length > 0 - ? process.env.OPENAI_API_KEYS - : undefined - const openAIApiKeyRaw = process.env.OPENAI_API_KEY?.trim() - const openAIApiKeyValues = parseCredentialList(openAIApiKeyRaw) - const openAIApiKey = openAIApiKeyValues[0] - const openAIApiKeyRawUsable = - openAIApiKeyValues.length > 0 ? openAIApiKeyRaw : undefined - const xaiOAuthToken = - isXaiRoute && - !this.providerOverride?.apiKey && - !routeCredential && - !openAIApiKeysPoolRaw && - !openAIApiKey - ? await resolveXaiAccessToken() - : undefined - const openAIApiKeyIsCopiedProviderKey = - Boolean( - openAIApiKeyRawUsable && - [ - process.env.OPENGATEWAY_API_KEY, - process.env.NVIDIA_API_KEY, - process.env.BNKR_API_KEY, - process.env.XAI_API_KEY, - process.env.MIMO_API_KEY, - process.env.VENICE_API_KEY, - process.env.MINIMAX_API_KEY, - process.env.ATLAS_CLOUD_API_KEY, - process.env.NEARAI_API_KEY, - process.env.FIREWORKS_API_KEY, - process.env.LONGCAT_API_KEY, - ].some(value => value?.trim() === openAIApiKeyRawUsable), - ) - const routeCredentialIsCopiedProviderKey = - Boolean( - routeCredential && - openAIApiKeyRawUsable && - routeCredential === openAIApiKeyRawUsable && - openAIApiKeyIsCopiedProviderKey, - ) - const routeCredentialIsProviderSpecific = - Boolean( - routeCredential && - (!openAIApiKeyRawUsable || - routeCredential !== openAIApiKeyRawUsable || - routeCredentialIsCopiedProviderKey), - ) - const routeCredentialIsGenericOpenAIFallback = - Boolean( - !routeCredentialIsProviderSpecific && - routeCredential && - openAIApiKeyRawUsable && - routeCredential === openAIApiKeyRawUsable, - ) - const copiedProviderCredential = - openAIApiKeyIsCopiedProviderKey && - (routeAcceptsGenericOpenAICredentials || routeCredentialIsCopiedProviderKey) - ? openAIApiKeyRawUsable - : undefined - const apiKeyRaw = - this.providerOverride?.apiKey ?? - copiedProviderCredential ?? - (routeCredentialIsGenericOpenAIFallback ? undefined : routeCredential) ?? - openAIApiKeysPoolRaw ?? - routeCredential ?? - (routeAcceptsGenericOpenAICredentials - ? openAIApiKeyRawUsable || xaiOAuthToken || '' - : '') - // A catalog-level auth header is part of the selected model's transport - // contract. Ignore global custom auth left behind by another route so it - // cannot replace that model-specific header or credential. - const catalogAuthHeader = - runtimeShimContext.catalogEntry?.transportOverrides?.openaiShim - ?.defaultAuthHeader - const configuredAuthHeaderValue = catalogAuthHeader - ? undefined - : process.env.OPENAI_AUTH_HEADER_VALUE?.trim() - if (configuredAuthHeaderValue && /[\r\n]/.test(configuredAuthHeaderValue)) { - throw new Error('OPENAI_AUTH_HEADER_VALUE must not contain CR/LF characters') - } - const customAuthHeader = catalogAuthHeader - ? undefined - : process.env.OPENAI_AUTH_HEADER?.trim() - const hasCustomAuthHeader = Boolean( - customAuthHeader && - /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(customAuthHeader), - ) - const explicitCustomAuthHeaderValue = hasCustomAuthHeader - ? configuredAuthHeaderValue - : '' - if (!explicitCustomAuthHeaderValue && hasInvalidCredentialPlaceholder(apiKeyRaw)) { - throw APIError.generate( - 401, - undefined, - buildOpenAICompatibilityErrorMessage( - 'OpenAI API error 401: invalid credential pool placeholder SUA_CHAVE detected', - { - category: 'auth_invalid', - requestUrl: request.baseUrl, - }, - ), - new Headers(), - ) - } - // Reads live process.env by design; must agree with the responses - // auto-route gate's processEnv (both default to process.env today). - const isAzure = isAzureStyleBaseUrl(request.baseUrl, requestProcessEnv) - - let isBankr = false - try { - isBankr = - runtimeShimContext.routeId === 'bankr' || - request.baseUrl.toLowerCase().includes('bankr') - } catch { /* malformed URL — not Bankr */ } - - const credentialPool = explicitCustomAuthHeaderValue - ? null - : this.getCredentialPool(apiKeyRaw) - const singleAuthValue = - explicitCustomAuthHeaderValue || parseCredentialList(apiKeyRaw)[0] || apiKeyRaw - - const buildHeadersForAttempt = async ( - credentialLease: CredentialLease | null, - ): Promise> => { - const headers: Record = { ...baseHeaders } - const authValue = - explicitCustomAuthHeaderValue || - refreshedCopilotToken || - credentialLease?.value || - (credentialPool ? '' : singleAuthValue) - - if (authValue) { - if (hasCustomAuthHeader && customAuthHeader) { - const defaultCustomAuthScheme = - customAuthHeader.toLowerCase() === 'authorization' ? 'bearer' : 'raw' - const customAuthScheme = - process.env.OPENAI_AUTH_SCHEME === 'raw' || - process.env.OPENAI_AUTH_SCHEME === 'bearer' - ? process.env.OPENAI_AUTH_SCHEME - : defaultCustomAuthScheme - headers[customAuthHeader] = - customAuthScheme === 'bearer' - ? `Bearer ${authValue}` - : authValue - } else if (isAzure) { - // Azure uses api-key header instead of Bearer token - headers['api-key'] = authValue - } else if (isBankr) { - // Bankr uses X-API-Key header instead of Bearer token - headers['X-API-Key'] = authValue - } else if (shimConfig.defaultAuthHeader?.name) { - headers[shimConfig.defaultAuthHeader.name] = - shimConfig.defaultAuthHeader.scheme === 'bearer' - ? `Bearer ${authValue}` - : authValue - } else { - headers.Authorization = `Bearer ${authValue}` - } - } else if (isGemini) { - const geminiCredential = await resolveGeminiCredential(process.env) - if (geminiCredential.kind !== 'none') { - headers.Authorization = `Bearer ${geminiCredential.credential}` - if (geminiCredential.kind !== 'api-key' && 'projectId' in geminiCredential && geminiCredential.projectId) { - headers['x-goog-user-project'] = geminiCredential.projectId - } - } - } - - if (isGithubCopilot) { - Object.assign(headers, COPILOT_HEADERS) - } else if (isGithubModels) { - headers['Accept'] = 'application/vnd.github+json' - headers['X-GitHub-Api-Version'] = '2022-11-28' - } - - // xAI / Grok prompt caching. Pinning the session id via x-grok-conv-id - // routes follow-up requests to the same backend so xAI can reuse the - // cached system prompt and conversation history. Mirrors the Hermes - // implementation (RELEASE_v0.8.0 PR #5604). - if (isXaiRoute) { - headers['x-grok-conv-id'] ??= getSessionId() - } - - return headers - } - - const buildChatCompletionsUrl = (baseUrl: string): string => { - // Azure Cognitive Services / Azure OpenAI require a deployment-specific - // path and an api-version query parameter. - if (isAzure) { - const normalizedBaseUrl = (baseUrl.split(/[?#]/, 1)[0] ?? baseUrl).replace(/\/+$/, '') - const apiVersion = process.env.AZURE_OPENAI_API_VERSION ?? '2024-12-01-preview' - const deployment = encodeURIComponent(request.resolvedModel ?? process.env.OPENAI_MODEL ?? 'gpt-4o') - - // If base URL already contains /deployments/, use it as-is with api-version. - if (/\/deployments\//i.test(normalizedBaseUrl)) { - return `${normalizedBaseUrl}/chat/completions?api-version=${apiVersion}` - } - - // Strip trailing /v1 or /openai/v1 if present, then build Azure path. - const normalizedBase = normalizedBaseUrl - .replace(/\/(openai\/)?v1\/?$/, '') - .replace(/\/+$/, '') - - return `${normalizedBase}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}` - } - - const normalizedBase = baseUrl.replace(/\/+$/, '') - // LongCat documents both `/openai/v1/chat/completions` and the - // CodeBuddy-specific `/openai/chat/completions` endpoint forms. - if ( - runtimeShimContext.routeId === 'longcat' && - isLongcatBaseUrl(normalizedBase) && - /^\/openai\/?$/.test(new URL(normalizedBase).pathname) - ) { - return `${normalizedBase}/v1/chat/completions` - } - if ( - runtimeShimContext.routeId === 'longcat' && - isLongcatBaseUrl(normalizedBase) && - /^\/openai(?:\/v1)?\/chat\/completions$/.test( - new URL(normalizedBase).pathname, - ) - ) { - return normalizedBase - } - return `${normalizedBase}/chat/completions` - } - - // Azure serves the Responses API only on the v1 surface - // ({resource}/openai/v1/responses — model in the request body, no - // api-version, no deployment-scoped form), so any Azure-style base is - // normalized to it: trailing /openai/v1, /v1, and - // /openai/deployments/ segments are stripped until stable (bases - // can carry several, e.g. /openai/deployments//openai/v1), then - // /openai/v1/responses is appended. - // https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses - const buildResponsesUrl = (baseUrl: string): string => { - const trimmedBase = baseUrl.replace(/\/+$/, '') - if (!isAzure) { - return `${trimmedBase}/responses` - } - let normalizedBase = (trimmedBase.split(/[?#]/, 1)[0] ?? trimmedBase).replace(/\/+$/, '') - for (;;) { - const stripped = normalizedBase - .replace(/\/(openai\/)?v1$/i, '') - .replace(/\/openai\/deployments\/[^/]+$/i, '') - .replace(/\/+$/, '') - if (stripped === normalizedBase) break - normalizedBase = stripped - } - return `${normalizedBase}/openai/v1/responses` - } - - const localRetryBaseUrls = isLocal - ? getLocalProviderRetryBaseUrls(request.baseUrl) - : [] - - const buildRequestUrl = (baseUrl: string): string => { - if (shimConfig.endpointPath) { - return `${baseUrl}${shimConfig.endpointPath}` - } - if (useNativeOllamaChat) { - return buildOllamaChatUrl(baseUrl) - } - return request.transport === 'responses' || request.transport === 'responses_compat' - ? buildResponsesUrl(baseUrl) - : buildChatCompletionsUrl(baseUrl) - } - - let activeBaseUrl = request.baseUrl - let requestUrl = buildRequestUrl(activeBaseUrl) - const attemptedLocalBaseUrls = new Set([activeBaseUrl]) - const attemptedLocalRequestUrls = new Set([requestUrl]) - let didRetryWithoutTools = false - let didRetryWithoutToolStream = false - let retryCredentialLease: CredentialLease | null = null - let didRefreshCopilotToken = false - let refreshedCopilotToken: string | undefined - - const promoteNextLocalBaseUrl = ( - reason: 'endpoint_not_found' | 'localhost_resolution_failed', - ): boolean => { - for (const candidateBaseUrl of localRetryBaseUrls) { - if (attemptedLocalBaseUrls.has(candidateBaseUrl)) { - continue - } - - attemptedLocalBaseUrls.add(candidateBaseUrl) - const candidateRequestUrl = buildRequestUrl(candidateBaseUrl) - if (attemptedLocalRequestUrls.has(candidateRequestUrl)) { - continue - } - - const previousUrl = requestUrl - attemptedLocalRequestUrls.add(candidateRequestUrl) - activeBaseUrl = candidateBaseUrl - requestUrl = candidateRequestUrl - - logForDebugging( - `[OpenAIShim] self-heal retry reason=${reason} method=POST from=${redactUrlForDiagnostics(previousUrl)} to=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, - { level: 'warn' }, - ) - - return true - } - - return false - } - - let serializedBody = '' - let imageClassificationCache: - | { serializedBody: string; hasImages: boolean } - | undefined - const bodyContainsImages = ( - bodyToInspect = serializedBody, - ): boolean => { - if (imageClassificationCache?.serializedBody === bodyToInspect) { - return imageClassificationCache.hasImages - } - - let hasImages = false - try { - const payload = JSON.parse(bodyToInspect) - if (payload && typeof payload === 'object' && !Array.isArray(payload)) { - hasImages = requestBodyContainsImages( - payload as Record, - ) - } - } catch (error) { - // Request serialization already succeeded before this error path, so - // parsing should only fail if a future caller passes a different body. - logForDebugging( - `[OpenAIShim] failed to inspect serialized request body for images: ${error instanceof Error ? error.message : String(error)}`, - { level: 'warn' }, - ) - } - - imageClassificationCache = { - serializedBody: bodyToInspect, - hasImages, - } - return hasImages - } - - // Extraction boundary: the executor lazily rebuilds planner-owned request bodies. - // Keep this marker stable so executor and planner extractions stay disjoint. - serializedBody = serializeBody() - - const refreshSerializedBody = (): void => { - serializedBody = serializeBody() - } - - const buildFetchInit = (headers: Record) => ({ - method: 'POST' as const, - headers, - body: serializedBody, + return executeOpenAIRequest({ + defaultHeaders: this.defaultHeaders, + providerOverride: this.providerOverride, + getCredentialPool: value => this.getCredentialPool(value), + filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, fetchWithProxyRetry, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools, }) - - const fetchAttemptWithHeadersDeadline = ( - url: string, - init: RequestInit, - ): Promise => - fetchWithHeadersDeadline(url, init, { - callerSignal: options?.signal, - timeoutMs: apiTimeoutMs, - }) - - const maxSelfHealAttempts = isLocal - ? localRetryBaseUrls.length + 1 - : 0 - const credentialPoolAttempts = credentialPool?.size ?? 1 - let maxAttempts = Math.max( - 2, - Math.max(isGithub ? GITHUB_429_MAX_RETRIES : 1, credentialPoolAttempts) + - maxSelfHealAttempts, - ) - - const throwClassifiedTransportError = ( - error: unknown, - requestUrl: string, - preclassifiedFailure?: ReturnType, - ): never => { - if (options?.signal?.aborted) { - throw preserveCallerAbortError(error, options.signal) - } - - throw createClassifiedTransportError( - error, - requestUrl, - request.resolvedModel, - preclassifiedFailure, - ) - } - - const throwClassifiedHttpError = ( - status: number, - errorBody: string, - parsedBody: object | undefined, - responseHeaders: Headers, - requestUrl: string, - rateHint = '', - preclassifiedFailure?: ReturnType, - ): never => { - const failure = - preclassifiedFailure ?? - classifyOpenAIHttpFailure({ - status, - body: errorBody, - url: requestUrl, - hasImages: bodyContainsImages(), - }) - const failureWithUrl = { ...failure, requestUrl: failure.requestUrl ?? requestUrl } - const redactedUrl = redactUrlForDiagnostics(requestUrl) - - logForDebugging( - `[OpenAIShim] request failed category=${failure.category} retryable=${failure.retryable} status=${status} method=POST url=${redactedUrl} model=${request.resolvedModel}`, - { level: 'warn' }, - ) - - throw APIError.generate( - status, - parsedBody, - buildOpenAICompatibilityErrorMessage( - `OpenAI API error ${status}: ${errorBody}${rateHint}`, - failureWithUrl, - ), - headersWithRequestUrl(responseHeaders, requestUrl), - ) - } - - let response: Response | undefined - const provider = request.baseUrl.includes('nvidia') ? 'nvidia-nim' - : request.baseUrl.includes('minimax') ? 'minimax' - : request.baseUrl.includes('xiaomimimo') || request.baseUrl.includes('mimo-v2') ? 'xiaomi-mimo' - : request.baseUrl.includes('localhost:11434') || request.baseUrl.includes('localhost:11435') ? 'ollama' - : request.baseUrl.includes('anthropic') ? 'anthropic' - : 'openai' - const { correlationId, startTime } = logApiCallStart(provider, request.resolvedModel) - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const credentialLease = retryCredentialLease ?? credentialPool?.next() ?? null - retryCredentialLease = null - if (credentialPool && !credentialLease) { - throw APIError.generate( - 401, - undefined, - buildOpenAICompatibilityErrorMessage( - 'OpenAI API error 401: credential pool exhausted after authentication failures', - { - category: 'auth_invalid', - requestUrl, - }, - ), - new Headers(), - ) - } - const headers = await buildHeadersForAttempt(credentialLease) - try { - response = await fetchAttemptWithHeadersDeadline( - requestUrl, - buildFetchInit(headers), - ) - } catch (error) { - if (options?.signal?.aborted) { - throw preserveCallerAbortError(error, options.signal) - } - const isResponseHeadersTimeout = - error instanceof ResponseHeadersTimeoutError - if (!isResponseHeadersTimeout && isAbortError(error)) { - throw error - } - - const classifiedFailure = classifyOpenAINetworkFailure(error, { - url: requestUrl, - }) - - if (isResponseHeadersTimeout) { - throwClassifiedTransportError(error, requestUrl, { - ...classifiedFailure, - retryable: false, - }) - } - - if ( - isLocal && - classifiedFailure.category === 'localhost_resolution_failed' && - promoteNextLocalBaseUrl('localhost_resolution_failed') - ) { - continue - } - - throwClassifiedTransportError(error, requestUrl, classifiedFailure) - } - - // After the try/catch, response is guaranteed to be defined — the catch - // block always throws (throwClassifiedTransportError returns never). - if (!response) continue - - if (response.ok) { - credentialPool?.reportSuccess(credentialLease) - if (useNativeOllamaChat) { - response = params.stream - ? convertOllamaStreamingResponse(response, request.resolvedModel) - : await convertOllamaNonStreamingResponse(response, request.resolvedModel) - } - let tokensIn = 0 - let tokensOut = 0 - // Skip clone() for streaming responses - it blocks until full body is received, - // defeating the purpose of streaming. Usage data is already sent via - // stream_options: { include_usage: true } and can be extracted from the stream. - if (!params.stream) { - try { - const bodyText = await response.text() - // Preserve routing metadata that `new Response()` drops to "". - // create() reads `response.url` to route between /responses, - // /messages, and Gemini conversion paths; losing it makes - // descriptor routes (OpenCode /messages, Gemini /models/gemini-*) - // fall through to the generic OpenAI converter and return the - // wrong message shape. `url` is a read-only getter on the - // prototype, so shadow it with an own property. - const originalUrl = response.url - const originalType = response.type - // Recreate the response immediately after reading the body, before - // JSON.parse — if parsing fails, downstream code can still read the - // body from the fresh Response instead of hitting "Body already used". - response = new Response(bodyText, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }) - if (originalUrl) { - try { - Object.defineProperty(response, 'url', { - value: originalUrl, - configurable: true, - }) - } catch { - /* some runtimes lock the property; routing falls back to transport */ - } - } - if (originalType && originalType !== 'basic') { - try { - Object.defineProperty(response, 'type', { - value: originalType, - configurable: true, - }) - } catch { - /* non-fatal: type is not used for response routing */ - } - } - const data = JSON.parse(bodyText) - tokensIn = data.usage?.prompt_tokens ?? 0 - tokensOut = data.usage?.completion_tokens ?? 0 - } catch { /* ignore — response is already recreated with the body intact */ } - } - logApiCallEnd(correlationId, startTime, request.resolvedModel, 'success', tokensIn, tokensOut, false) - return response - } - - if ( - isGithub && - response.status === 429 && - attempt < maxAttempts - 1 - ) { - await response.text().catch(() => {}) - const delaySec = Math.min( - GITHUB_429_BASE_DELAY_SEC * 2 ** attempt, - GITHUB_429_MAX_DELAY_SEC, - ) - await sleepMs(delaySec * 1000) - continue - } - // Read body exactly once here — Response body is a stream that can only - // be consumed a single time. - const errorBody = await response.text().catch(() => 'unknown error') - const rateHint = - isGithub && response.status === 429 ? formatRetryAfterHint(response) : '' - - // If GitHub Copilot returns error about /chat/completions, - // try the /responses endpoint (needed for GPT-5+ models) - if (isGithub && response.status === 400) { - if (errorBody.includes('/chat/completions') || errorBody.includes('not accessible')) { - const responsesUrl = `${request.baseUrl}/responses` - const responsesBody = buildResponsesBody() - const responsesSerializedBody = stableStringifyJson(responsesBody) - - let responsesResponse!: Response - try { - responsesResponse = await fetchAttemptWithHeadersDeadline( - responsesUrl, - { - method: 'POST', - headers, - body: responsesSerializedBody, - }, - ) - } catch (error) { - if (options?.signal?.aborted) { - throw preserveCallerAbortError(error, options.signal) - } - if ( - !(error instanceof ResponseHeadersTimeoutError) && - isAbortError(error) - ) { - throw error - } - const classifiedFailure = classifyOpenAINetworkFailure(error, { - url: responsesUrl, - }) - if (error instanceof ResponseHeadersTimeoutError) { - throwClassifiedTransportError(error, responsesUrl, { - ...classifiedFailure, - retryable: false, - }) - } - throwClassifiedTransportError( - error, - responsesUrl, - classifiedFailure, - ) - } - - if (responsesResponse.ok) { - return responsesResponse - } - const responsesErrorBody = await responsesResponse.text().catch(() => 'unknown error') - const responsesFailure = classifyOpenAIHttpFailure({ - status: responsesResponse.status, - body: responsesErrorBody, - hasImages: bodyContainsImages(responsesSerializedBody), - }) - let responsesErrorResponse: object | undefined - try { responsesErrorResponse = JSON.parse(responsesErrorBody) } catch { /* raw text */ } - throwClassifiedHttpError( - responsesResponse.status, - responsesErrorBody, - responsesErrorResponse, - responsesResponse.headers, - responsesUrl, - '', - responsesFailure, - ) - } - } - - const failure = classifyOpenAIHttpFailure({ - status: response.status, - body: errorBody, - hasImages: bodyContainsImages(), - }) - - // GitHub Copilot 401 with expired token: force-refresh and retry once. - // Only applies to the Copilot endpoint, not GitHub Models API or custom - // routes, and only when the failing credential is the stored Copilot - // token (not a provider override, route credential, or custom auth). - // The refreshed token is stored in refreshedCopilotToken so the next - // iteration's buildHeadersForAttempt picks it up instead of the stale - // singleAuthValue captured before the loop. - if (isGithubCopilot && response.status === 401 && !didRefreshCopilotToken) { - if (isCopilotTokenExpiredError(errorBody)) { - const oldToken = headers.Authorization?.replace(/^Bearer\s+/i, '') || '' - if (oldToken && oldToken === (process.env.OPENAI_API_KEY ?? '')) { - didRefreshCopilotToken = true - const refreshed = await refreshCopilotTokenOn401() - if (refreshed) { - const newApiKey = process.env.OPENAI_API_KEY?.trim() || '' - if (newApiKey && newApiKey !== oldToken) { - refreshedCopilotToken = newApiKey - } - if (attempt < maxAttempts - 1) { - continue - } - } - } - } - } - - const credentialFailureKind = - failure.category === 'auth_invalid' && !failure.retryable - ? 'auth' - : response.status === 402 || response.status === 429 - ? 'cooldown' - : null - if (credentialPool && credentialPool.size > 1 && credentialFailureKind) { - credentialPool.reportFailure( - credentialLease, - credentialFailureKind, - CREDENTIAL_POOL_COOLDOWN_MS, - ) - if (attempt < maxAttempts - 1) { - logForDebugging( - `[OpenAIShim] credential pool retry status=${response.status} method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, - { level: 'warn' }, - ) - continue - } - } - - const shouldRetryLocalEndpoint404 = - failure.category === 'endpoint_not_found' || - ( - response.status === 404 && - failure.category === 'vision_not_supported' - ) - if ( - isLocal && - shouldRetryLocalEndpoint404 && - promoteNextLocalBaseUrl('endpoint_not_found') - ) { - continue - } - - const hasToolsPayload = - effectiveTransport === 'responses' || effectiveTransport === 'responses_compat' || effectiveTransport === 'anthropic_messages' || effectiveTransport === 'gemini' - ? Array.isArray(params.tools) && params.tools.length > 0 - : Array.isArray(body.tools) && body.tools.length > 0 - - if ( - !didRetryWithoutTools && - failure.category === 'tool_call_incompatible' && - shouldAttemptLocalToollessRetry({ - baseUrl: activeBaseUrl, - hasTools: hasToolsPayload, - }) - ) { - didRetryWithoutTools = true - delete body.tools - delete body.tool_choice - delete body.tool_stream - omitTools.responses = true - omitTools.anthropic = true - omitTools.gemini = true - refreshSerializedBody() - - logForDebugging( - `[OpenAIShim] self-heal retry reason=tool_call_incompatible mode=toolless method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, - { level: 'warn' }, - ) - continue - } - - // `tool_stream` self-heal (#1950): some OpenAI-compatible gateways (e.g. - // NVIDIA NIM) reject the Z.AI-proprietary `tool_stream` parameter with a - // 400. Drop only that parameter and retry with tools intact — streaming - // tool calls simply aren't streamed on such gateways. This guards against - // regressions where the parameter slips through the catalog/runtime - // gating that normally suppresses it. - if ( - !didRetryWithoutToolStream && - failure.category === 'tool_stream_unsupported' && - body.tool_stream === true - ) { - didRetryWithoutToolStream = true - // Reserve one additional request only after this specific recovery is - // needed. Increasing the shared initial budget changes unrelated - // GitHub and credential-pool retry behavior. - maxAttempts += 1 - delete body.tool_stream - refreshSerializedBody() - // This retry only changes request formatting. Reuse the credential that - // received the rejection so a pool with unequal model access cannot - // turn a recoverable 400 into an unrelated authorization failure. - retryCredentialLease = credentialLease - - logForDebugging( - `[OpenAIShim] self-heal retry reason=tool_stream_unsupported method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, - { level: 'warn' }, - ) - continue - } - - let errorResponse: object | undefined - try { errorResponse = JSON.parse(errorBody) } catch { /* raw text */ } - throwClassifiedHttpError( - response.status, - errorBody, - errorResponse, - response.headers as unknown as Headers, - requestUrl, - rateHint, - failure, - ) - } - - throw APIError.generate( - 500, undefined, 'OpenAI shim: request loop exited unexpectedly', - new Headers(), - ) - // Extraction boundary: request execution | response conversion façade. - // Response conversion methods below remain façade-owned until their own extraction. - // Keep this marker stable so adjacent independent deletions do not overlap. } private _convertNonStreamingResponse( diff --git a/src/services/api/openaiShim/architecture.test.ts b/src/services/api/openaiShim/architecture.test.ts new file mode 100644 index 0000000000..6bc75dd955 --- /dev/null +++ b/src/services/api/openaiShim/architecture.test.ts @@ -0,0 +1,35 @@ +import { existsSync, readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { describe, expect, test } from 'bun:test' + +const facadePath = fileURLToPath(new URL('../openaiShim.ts', import.meta.url)) +const moduleDirectory = fileURLToPath(new URL('.', import.meta.url)) + +// The shared extraction seam is 5,123 lines. Each independently mergeable PR +// removes at least this many net lines when its representative module exists. +// Detecting modules makes the ceiling tighten regardless of merge order. +const extractionDeltas = [ + ['streamControl.ts', 169], + ['providerCompatibility.ts', 115], + ['ollamaAdapter.ts', 387], + ['messageConversion.ts', 474], + ['rawToolCallParsing.ts', 291], + ['xmlToolCallParsing.ts', 356], + ['streamConversion.ts', 1_072], + ['clientDispatch.ts', 182], + ['requestPlanner.ts', 304], + ['requestExecutor.ts', 716], +] as const + +describe('openaiShim façade architecture', () => { + test('does not regain logic removed by the independent extractions', () => { + const activeReduction = extractionDeltas.reduce( + (total, [moduleName, reduction]) => + total + (existsSync(`${moduleDirectory}/${moduleName}`) ? reduction : 0), + 0, + ) + const facadeLines = readFileSync(facadePath, 'utf8').trimEnd().split('\n').length + + expect(facadeLines).toBeLessThanOrEqual(5_123 - activeReduction) + }) +}) diff --git a/src/services/api/openaiShim/requestExecutor.integration.test.ts b/src/services/api/openaiShim/requestExecutor.integration.test.ts new file mode 100644 index 0000000000..1ebfe9d5dc --- /dev/null +++ b/src/services/api/openaiShim/requestExecutor.integration.test.ts @@ -0,0 +1,4709 @@ +import { APIError } from '@anthropic-ai/sdk' +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../../test/sharedMutationLock.js' +import { asMockFetch } from '../../../test/typedMocks.js' +import { _clearRegistryForTesting, ensureIntegrationsLoaded, registerGateway } from '../../../integrations/index.ts' +import { applyProviderFlag } from '../../../utils/providerFlag.ts' +import { applyProviderProfileToProcessEnv } from '../../../utils/providerProfiles.ts' +import { + getAssistantMessageFromError, + OPENCODE_GO_FREE_LIMIT_ERROR_MESSAGE, +} from '../errors.ts' +import { createOpenAIShimClient } from '../openaiShim.ts' + +type FetchType = typeof globalThis.fetch + +const originalEnv = { + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_API_BASE: process.env.OPENAI_API_BASE, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + OPENAI_API_KEYS: process.env.OPENAI_API_KEYS, + OPENAI_MODEL: process.env.OPENAI_MODEL, + OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT, + OPENAI_AUTH_HEADER: process.env.OPENAI_AUTH_HEADER, + OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, + OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE, + CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, + GITHUB_COPILOT_KEY: process.env.GITHUB_COPILOT_KEY, + GITHUB_ENTERPRISE_URL: process.env.GITHUB_ENTERPRISE_URL, + GITHUB_TOKEN: process.env.GITHUB_TOKEN, + GH_TOKEN: process.env.GH_TOKEN, + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, + GEMINI_ACCESS_TOKEN: process.env.GEMINI_ACCESS_TOKEN, + GEMINI_AUTH_MODE: process.env.GEMINI_AUTH_MODE, + GEMINI_BASE_URL: process.env.GEMINI_BASE_URL, + GEMINI_MODEL: process.env.GEMINI_MODEL, + GOOGLE_CLOUD_PROJECT: process.env.GOOGLE_CLOUD_PROJECT, + ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS, + NVIDIA_API_KEY: process.env.NVIDIA_API_KEY, + NVIDIA_NIM: process.env.NVIDIA_NIM, + MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + BNKR_API_KEY: process.env.BNKR_API_KEY, + BANKR_BASE_URL: process.env.BANKR_BASE_URL, + BANKR_MODEL: process.env.BANKR_MODEL, + OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, + DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY, + MIMO_API_KEY: process.env.MIMO_API_KEY, + OPENGATEWAY_API_KEY: process.env.OPENGATEWAY_API_KEY, + OPENGATEWAY_BASE_URL: process.env.OPENGATEWAY_BASE_URL, + OPENCODE_API_KEY: process.env.OPENCODE_API_KEY, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED: process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID: process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, + CLAUDE_STREAM_IDLE_TIMEOUT_MS: process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS, +} + +const originalFetch = globalThis.fetch + +function restoreEnv(key: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } +} + +type OpenAIShimClient = { + beta: { + messages: { + create: ( + params: Record, + options?: Record, + ) => Promise & { + withResponse: () => Promise<{ data: AsyncIterable> }> + } + } + } +} + +function makeSseResponse(lines: string[]): Response { + const encoder = new TextEncoder() + return new Response( + new ReadableStream({ + start(controller) { + for (const line of lines) { + controller.enqueue(encoder.encode(line)) + } + controller.close() + }, + }), + { + headers: { + 'Content-Type': 'text/event-stream', + }, + }, + ) +} + +function withResponseUrl(response: Response, url: string): Response { + Object.defineProperty(response, 'url', { + value: url, + configurable: true, + }) + return response +} + +type StallingResponse = { + response: Response + cancelReasons: unknown[] + close: () => void +} + +function makeStallingResponse( + firstChunk: string, + url = 'https://api.example.test/v1/chat/completions', + contentType = 'text/event-stream', +): StallingResponse { + const encoder = new TextEncoder() + const cancelReasons: unknown[] = [] + let streamController: ReadableStreamDefaultController | undefined + let closed = false + + const response = new Response( + new ReadableStream({ + start(controller) { + streamController = controller + controller.enqueue(encoder.encode(firstChunk)) + }, + cancel(reason) { + closed = true + cancelReasons.push(reason) + }, + }), + { + headers: { + 'Content-Type': contentType, + }, + }, + ) + + return { + response: withResponseUrl(response, url), + cancelReasons, + close: () => { + if (closed) return + closed = true + try { + streamController?.close() + } catch { + // The test may already have cancelled the stream. + } + }, + } +} + +type ShimStream = AsyncIterable> & { + controller: AbortController +} + +type StreamDrainOutcome = + | { status: 'completed'; events: Array> } + | { + status: 'rejected' + events: Array> + error: unknown + } + +async function waitForPromise( + promise: Promise, + timeoutMs: number, + message: string, +): Promise { + let timer: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), timeoutMs) + }), + ]) + } finally { + if (timer !== undefined) { + clearTimeout(timer) + } + } +} + +async function expectAbortStopsStream({ + abort, + cancelReasons, + expectedEventsBeforeAbort, + label, + stream, +}: { + abort: () => void + cancelReasons: unknown[] + expectedEventsBeforeAbort: number + label: string + stream: ShimStream +}): Promise { + const events: Array> = [] + let resolveReady!: () => void + const ready = new Promise(resolve => { + resolveReady = resolve + }) + + const drain = (async (): Promise => { + try { + for await (const event of stream) { + events.push(event) + if (events.length >= expectedEventsBeforeAbort) { + resolveReady() + } + } + return { status: 'completed', events } + } catch (error) { + return { status: 'rejected', events, error } + } + })() + + await waitForPromise( + ready, + 500, + `${label} did not produce initial stream events`, + ) + // Let the for-await loop ask the stream reader for the next chunk, so the + // abort has to wake a real pending read rather than only flipping a flag. + await Promise.resolve() + await Promise.resolve() + + abort() + + const outcome = await waitForPromise( + drain, + 500, + `${label} did not stop promptly after abort`, + ) + expect(cancelReasons).toHaveLength(1) + expect(outcome.status).toBe('rejected') + if (outcome.status === 'rejected') { + expect((outcome.error as { name?: unknown }).name).toBe('AbortError') + } + return outcome +} + +async function expectPausedAbortCancelsStream({ + cancelReasons, + label, + stream, +}: { + cancelReasons: unknown[] + label: string + stream: ShimStream +}): Promise>> { + const iterator = stream[Symbol.asyncIterator]() + const first = await waitForPromise( + iterator.next(), + 500, + `${label} did not produce first stream event`, + ) + expect(first.done).toBe(false) + + stream.controller.abort() + await waitForPromise( + (async () => { + for (let i = 0; i < 10; i++) { + if (cancelReasons.length > 0) return + await Promise.resolve() + } + throw new Error(`${label} did not cancel source on controller abort`) + })(), + 500, + `${label} did not cancel source on controller abort`, + ) + + const returned = await waitForPromise( + Promise.resolve(iterator.return?.()), + 500, + `${label} did not return promptly after abort while paused`, + ) + expect(cancelReasons).toHaveLength(1) + return returned as IteratorResult> +} + +async function expectBufferedAbortRejectsNext({ + expectedText, + label, + stream, +}: { + expectedText?: string + label: string + stream: ShimStream +}): Promise { + const iterator = stream[Symbol.asyncIterator]() + + try { + let firstDelta: Record | undefined + for (let i = 0; i < 5; i++) { + const next = await waitForPromise( + iterator.next(), + 500, + `${label} did not produce expected pre-abort events`, + ) + expect(next.done).toBe(false) + if (next.value?.type === 'content_block_delta') { + firstDelta = next.value + break + } + } + + expect(firstDelta).toBeDefined() + if (expectedText !== undefined) { + expect((firstDelta as { delta?: { text?: string } }).delta?.text).toBe(expectedText) + } + + stream.controller.abort() + const afterAbort = await waitForPromise( + iterator.next().then( + value => ({ status: 'resolved' as const, value }), + error => ({ status: 'rejected' as const, error }), + ), + 500, + `${label} did not stop after abort`, + ) + + if (afterAbort.status !== 'rejected') { + throw new Error(`${label} yielded after abort: ${JSON.stringify(afterAbort.value)}`) + } + expect((afterAbort.error as { name?: unknown }).name).toBe('AbortError') + } finally { + await Promise.resolve(iterator.return?.()).catch(() => {}) + } +} + +function makeOpenAIStreamFrame( + delta: Record, + finishReason: string | null = null, +): string { + return `data: ${JSON.stringify({ + id: 'chatcmpl-abort-test', + object: 'chat.completion.chunk', + created: 1_780_000_000, + model: 'test-model', + choices: [{ index: 0, delta, finish_reason: finishReason }], + })}\n\n` +} + +function makeStreamChunks(chunks: unknown[]): string[] { + return [ + ...chunks.map(chunk => `data: ${JSON.stringify(chunk)}\n\n`), + 'data: [DONE]\n\n', + ] +} + +function importFreshOpenAIShim( + cacheKey: string, +): Promise { + return import(`../openaiShim.ts?${cacheKey}`) +} + +type StreamIdleTestApi = { + StreamIdleTimeoutError: new (timeoutMs: number) => Error + getStreamIdleTimeoutMs: () => number + readWithIdleTimeout: ( + reader: ReadableStreamDefaultReader, + timeoutMs: number, + options?: { signal?: AbortSignal; onTimeout?: () => void }, + ) => Promise['read']>>> +} + +async function getStreamIdleTestApi(cacheKey: string): Promise { + const mod = await importFreshOpenAIShim(cacheKey) + const testApi = mod.__test as unknown as Partial + expect(typeof testApi.StreamIdleTimeoutError).toBe('function') + expect(typeof testApi.getStreamIdleTimeoutMs).toBe('function') + expect(typeof testApi.readWithIdleTimeout).toBe('function') + return testApi as StreamIdleTestApi +} + +function makeChatCompletionResponse(model: string): Response { + return new Response( + JSON.stringify({ + id: 'chatcmpl-test', + model, + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) +} + +async function captureChatCompletionRequest( + model = 'mimo-v2.5-pro', +): Promise<{ authorization: string | null; url: string | null }> { + let authorization: string | null = null + let url: string | null = null + + globalThis.fetch = (async (input, init) => { + url = String(input) + const headers = init?.headers as Record | undefined + authorization = headers?.Authorization ?? headers?.authorization ?? null + + return makeChatCompletionResponse(model) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + return { authorization, url } +} + +beforeEach(async () => { + await acquireSharedMutationLock('openaiShim.test.ts') + process.env.OPENAI_BASE_URL = 'http://example.test/v1' + delete process.env.OPENAI_API_BASE + process.env.OPENAI_API_KEY = 'test-key' + delete process.env.OPENAI_API_KEYS + delete process.env.OPENAI_MODEL + delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AUTH_HEADER + delete process.env.OPENAI_AUTH_SCHEME + delete process.env.OPENAI_AUTH_HEADER_VALUE + delete process.env.CLAUDE_CODE_USE_GITHUB + delete process.env.GITHUB_COPILOT_KEY + delete process.env.GITHUB_ENTERPRISE_URL + delete process.env.GITHUB_TOKEN + delete process.env.GH_TOKEN + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.GEMINI_API_KEY + delete process.env.GOOGLE_API_KEY + delete process.env.GEMINI_ACCESS_TOKEN + delete process.env.GEMINI_AUTH_MODE + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_MODEL + delete process.env.GOOGLE_CLOUD_PROJECT + delete process.env.ANTHROPIC_CUSTOM_HEADERS + delete process.env.NVIDIA_API_KEY + delete process.env.NVIDIA_NIM + delete process.env.MINIMAX_API_KEY + delete process.env.BNKR_API_KEY + delete process.env.BANKR_BASE_URL + delete process.env.BANKR_MODEL + delete process.env.OPENROUTER_API_KEY + delete process.env.DEEPSEEK_API_KEY + delete process.env.MIMO_API_KEY + delete process.env.OPENGATEWAY_API_KEY + delete process.env.OPENGATEWAY_BASE_URL + delete process.env.OPENCODE_API_KEY + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID + delete process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS +}) +afterEach(() => { + try { + restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL) + restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE) + restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY) + restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS) + restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL) + restoreEnv('OPENAI_API_FORMAT', originalEnv.OPENAI_API_FORMAT) + restoreEnv('OPENAI_AUTH_HEADER', originalEnv.OPENAI_AUTH_HEADER) + restoreEnv('OPENAI_AUTH_SCHEME', originalEnv.OPENAI_AUTH_SCHEME) + restoreEnv('OPENAI_AUTH_HEADER_VALUE', originalEnv.OPENAI_AUTH_HEADER_VALUE) + restoreEnv('CLAUDE_CODE_USE_GITHUB', originalEnv.CLAUDE_CODE_USE_GITHUB) + restoreEnv('GITHUB_COPILOT_KEY', originalEnv.GITHUB_COPILOT_KEY) + restoreEnv('GITHUB_ENTERPRISE_URL', originalEnv.GITHUB_ENTERPRISE_URL) + restoreEnv('GITHUB_TOKEN', originalEnv.GITHUB_TOKEN) + restoreEnv('GH_TOKEN', originalEnv.GH_TOKEN) + restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI) + restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI) + restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY) + restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY) + restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN) + restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE) + restoreEnv('GEMINI_BASE_URL', originalEnv.GEMINI_BASE_URL) + restoreEnv('GEMINI_MODEL', originalEnv.GEMINI_MODEL) + restoreEnv('GOOGLE_CLOUD_PROJECT', originalEnv.GOOGLE_CLOUD_PROJECT) + restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS) + restoreEnv('NVIDIA_API_KEY', originalEnv.NVIDIA_API_KEY) + restoreEnv('NVIDIA_NIM', originalEnv.NVIDIA_NIM) + restoreEnv('MINIMAX_API_KEY', originalEnv.MINIMAX_API_KEY) + restoreEnv('BNKR_API_KEY', originalEnv.BNKR_API_KEY) + restoreEnv('BANKR_BASE_URL', originalEnv.BANKR_BASE_URL) + restoreEnv('BANKR_MODEL', originalEnv.BANKR_MODEL) + restoreEnv('OPENROUTER_API_KEY', originalEnv.OPENROUTER_API_KEY) + restoreEnv('DEEPSEEK_API_KEY', originalEnv.DEEPSEEK_API_KEY) + restoreEnv('MIMO_API_KEY', originalEnv.MIMO_API_KEY) + restoreEnv('OPENGATEWAY_API_KEY', originalEnv.OPENGATEWAY_API_KEY) + restoreEnv('OPENGATEWAY_BASE_URL', originalEnv.OPENGATEWAY_BASE_URL) + restoreEnv('OPENCODE_API_KEY', originalEnv.OPENCODE_API_KEY) + restoreEnv('CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED) + restoreEnv('CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID', originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID) + restoreEnv('CLAUDE_STREAM_IDLE_TIMEOUT_MS', originalEnv.CLAUDE_STREAM_IDLE_TIMEOUT_MS) + globalThis.fetch = originalFetch + _clearRegistryForTesting() + ensureIntegrationsLoaded() + } finally { + releaseSharedMutationLock() + } +}) + + +test('uses OpenAI-compatible responses endpoint when OPENAI_API_FORMAT=responses', async () => { + process.env.OPENAI_API_FORMAT = 'responses' + let capturedUrl = '' + let capturedBody: Record | undefined + + globalThis.fetch = (async (input, init) => { + capturedUrl = String(input) + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'resp-1', + model: 'gpt-5.4', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok' }], + }, + ], + usage: { + input_tokens: 8, + output_tokens: 3, + total_tokens: 11, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-5.4', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedUrl).toBe('http://example.test/v1/responses') + expect(capturedBody?.model).toBe('gpt-5.4') + expect(capturedBody?.instructions).toBe('test system') + expect(capturedBody?.max_output_tokens).toBe(64) + expect(capturedBody?.store).toBe(false) + expect(capturedBody?.input).toEqual([ + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'hello' }], + }, + ]) +}) + +test('nests reasoning effort for OpenAI-compatible responses endpoint', async () => { + process.env.OPENAI_API_FORMAT = 'responses' + let capturedBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'resp-1', + model: 'gpt-5.4', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok' }], + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ reasoningEffort: 'high' }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-5.4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedBody?.reasoning).toEqual({ effort: 'high', summary: 'auto' }) + expect(capturedBody?.include).toEqual(['reasoning.encrypted_content']) + expect(capturedBody).not.toHaveProperty('reasoning_effort') + expect(capturedBody).not.toHaveProperty('reasoning_summary') +}) + +test('uses OpenAI-compatible responses endpoint with text chunk types when OPENAI_API_FORMAT=responses_compat', async () => { + process.env.OPENAI_API_FORMAT = 'responses_compat' + let capturedUrl = '' + let capturedBody: Record | undefined + + globalThis.fetch = (async (input, init) => { + capturedUrl = String(input) + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'resp-1', + model: 'gpt-5.4', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok' }], + }, + ], + usage: { + input_tokens: 8, + output_tokens: 3, + total_tokens: 11, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-5.4', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedUrl).toBe('http://example.test/v1/responses') + expect(capturedBody?.model).toBe('gpt-5.4') + expect(capturedBody?.instructions).toBe('test system') + expect(capturedBody?.max_output_tokens).toBe(64) + expect(capturedBody?.store).toBe(false) + expect(capturedBody?.input).toEqual([ + { + type: 'message', + role: 'user', + content: [{ type: 'text', text: 'hello' }], + }, + ]) +}) + +test('strips store from strict OpenAI-compatible responses providers', async () => { + process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' + process.env.OPENAI_API_FORMAT = 'responses' + let capturedUrl = '' + let capturedBody: Record | undefined + + globalThis.fetch = (async (input, init) => { + capturedUrl = String(input) + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'resp-1', + model: 'kimi-k2.5', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok' }], + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'kimi-k2.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedUrl).toBe('https://api.moonshot.ai/v1/responses') + expect(capturedBody?.store).toBeUndefined() +}) + +test('strips store when providerOverride routes chat_completions to the Gemini host', async () => { + let capturedBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + capturedBody = JSON.parse(String(init?.body)) as Record + return new Response( + JSON.stringify({ + id: 'chatcmpl-gemini', + choices: [{ message: { role: 'assistant', content: 'ok' } }], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + defaultHeaders: {}, + providerOverride: { + model: 'gemini-3.1-pro', + baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', + apiKey: 'gemini-key', + }, + }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gemini-3.1-pro', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedBody?.store).toBeUndefined() +}) + +test('strips store when providerOverride routes responses API to the Gemini host', async () => { + process.env.OPENAI_API_FORMAT = 'responses' + let capturedBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + capturedBody = JSON.parse(String(init?.body)) as Record + return new Response( + JSON.stringify({ + id: 'resp-gemini', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok' }], + }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + defaultHeaders: {}, + providerOverride: { + model: 'gemini-3.1-pro', + baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', + apiKey: 'gemini-key', + }, + }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gemini-3.1-pro', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedBody?.store).toBeUndefined() +}) + +test('uses custom OpenAI-compatible auth header value when configured', async () => { + process.env.OPENAI_API_KEY = 'generic-key' + process.env.OPENAI_AUTH_HEADER = 'api-key' + process.env.OPENAI_AUTH_HEADER_VALUE = 'hicap-header-value' + let capturedHeaders: Headers | undefined + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers as HeadersInit) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + choices: [{ message: { role: 'assistant', content: 'ok' } }], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedHeaders?.get('api-key')).toBe('hicap-header-value') + expect(capturedHeaders?.get('authorization')).toBeNull() +}) + +test('uses Hicap api-key auth header for the Hicap route', async () => { + process.env.OPENAI_API_KEY = 'hicap-live-key' + process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' + let capturedHeaders: Headers | undefined + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers as HeadersInit) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + choices: [{ message: { role: 'assistant', content: 'ok' } }], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'claude-opus-4.8', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedHeaders?.get('api-key')).toBe('hicap-live-key') + expect(capturedHeaders?.get('authorization')).toBeNull() +}) + +test('defaults Authorization custom auth header to bearer scheme', async () => { + process.env.OPENAI_API_KEY = 'authorization-key' + process.env.OPENAI_AUTH_HEADER = 'Authorization' + let capturedHeaders: Headers | undefined + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers as HeadersInit) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + choices: [{ message: { role: 'assistant', content: 'ok' } }], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedHeaders?.get('authorization')).toBe('Bearer authorization-key') +}) + +test('honors bearer scheme for custom OpenAI-compatible auth headers', async () => { + process.env.OPENAI_API_KEY = 'custom-key' + process.env.OPENAI_AUTH_HEADER = 'X-Custom-Authorization' + process.env.OPENAI_AUTH_SCHEME = 'bearer' + let capturedHeaders: Headers | undefined + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers as HeadersInit) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + choices: [{ message: { role: 'assistant', content: 'ok' } }], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedHeaders?.get('x-custom-authorization')).toBe('Bearer custom-key') + expect(capturedHeaders?.get('authorization')).toBeNull() +}) + +test('ignores custom auth header value when no custom header is configured', async () => { + delete process.env.OPENAI_API_KEY + process.env.OPENAI_AUTH_HEADER_VALUE = 'gateway-header-value' + let capturedHeaders: Headers | undefined + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers as HeadersInit) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + choices: [{ message: { role: 'assistant', content: 'ok' } }], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedHeaders?.get('authorization')).toBeNull() +}) + + +test('applies descriptor static headers before client and request headers', async () => { + let capturedHeaders: Headers | undefined + + registerGateway({ + id: 'shim-header-test', + label: 'Shim Header Test', + category: 'hosted', + defaultBaseUrl: 'https://shim-header-test.example/v1', + defaultModel: 'shim-test-model', + setup: { + requiresAuth: true, + authMode: 'api-key', + credentialEnvVars: ['OPENAI_API_KEY'], + }, + transportConfig: { + kind: 'openai-compatible', + openaiShim: { + headers: { + 'x-static-header': 'from-descriptor', + 'x-override-header': 'from-descriptor', + }, + }, + }, + }) + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://shim-header-test.example/v1' + process.env.OPENAI_MODEL = 'shim-test-model' + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'shim-test-model', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 8, + completion_tokens: 3, + total_tokens: 11, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + defaultHeaders: { + 'x-override-header': 'from-client', + }, + }) as OpenAIShimClient + + await client.beta.messages.create( + { + model: 'shim-test-model', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, + { + headers: { + 'x-override-header': 'from-request', + }, + }, + ) + + expect(capturedHeaders?.get('x-static-header')).toBe('from-descriptor') + expect(capturedHeaders?.get('x-override-header')).toBe('from-request') +}) + +test('opengateway sends Accept-Encoding: identity header on chat requests', async () => { + let capturedHeaders: Headers | undefined + + registerGateway({ + id: 'gitlawb-opengateway-test', + label: 'Gitlawb Opengateway', + category: 'aggregating', + defaultBaseUrl: 'https://opengateway.gitlawb.com/v1/xiaomi-mimo', + defaultModel: 'mimo-v2.5-pro', + setup: { + requiresAuth: false, + authMode: 'none', + }, + transportConfig: { + kind: 'openai-compatible', + openaiShim: { + headers: { + 'Accept-Encoding': 'identity', + }, + defaultAuthHeader: { + name: 'api-key', + scheme: 'raw', + }, + preserveReasoningContent: true, + requireReasoningContentOnAssistantMessages: true, + reasoningContentFallback: '', + maxTokensField: 'max_completion_tokens', + supportsApiFormatSelection: false, + supportsAuthHeaders: false, + }, + }, + }) + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1/xiaomi-mimo' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'mimo-v2.5-pro', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 8, + completion_tokens: 3, + total_tokens: 11, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create( + { + model: 'mimo-v2.5-pro', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, + {}, + ) + + expect(capturedHeaders?.get('Accept-Encoding')).toBe('identity') +}) + +test('uses direct GitHub Copilot Enterprise key for shim authentication', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.GITHUB_COPILOT_KEY = 'enterprise-direct-key' + process.env.GITHUB_ENTERPRISE_URL = 'https://github.mycompany.com' + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_BASE_URL + + const { authorization, url } = await captureChatCompletionRequest( + 'github:gpt-4o', + ) + + expect(authorization).toBe('Bearer enterprise-direct-key') + expect(url).toBe('https://github.mycompany.com/api/copilot/chat/completions') +}) + +test('direct GitHub Copilot key wins over stale OpenAI key', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.GITHUB_COPILOT_KEY = 'enterprise-direct-key' + process.env.GITHUB_ENTERPRISE_URL = 'https://github.mycompany.com' + process.env.OPENAI_API_KEY = 'stale-openai-key' + delete process.env.OPENAI_BASE_URL + + const { authorization } = await captureChatCompletionRequest( + 'github:gpt-4o', + ) + + expect(authorization).toBe('Bearer enterprise-direct-key') +}) + +// Extraction seam: stream conversion usage | shared stream control. + + +// Extraction seam: shared stream control | Gemini stream conversion. + +test('OpenAI-compatible stream rejects with idle timeout when it stalls after a chunk', async () => { + await getStreamIdleTestApi('stream-idle-openai-stall') + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' + const stalled = makeStallingResponse( + makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), + ) + + globalThis.fetch = (async () => stalled.response) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'glm-5.2', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + + const events: Array> = [] + const startedAt = Date.now() + let caught: unknown + try { + for await (const event of result.data) { + events.push(event) + } + } catch (error) { + caught = error + } finally { + stalled.close() + } + + expect(Date.now() - startedAt).toBeLessThan(500) + expect((caught as Error).name).toBe('StreamIdleTimeoutError') + expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') + const textDeltas = events.flatMap(event => { + const eventDelta = event.delta as { type?: string; text?: string } | undefined + return eventDelta?.type === 'text_delta' && typeof eventDelta.text === 'string' + ? [eventDelta.text] + : [] + }) + expect(textDeltas).toEqual(['partial']) +}) + +test('OpenAI-compatible stream keeps slow active chunks alive under the idle timeout', async () => { + await getStreamIdleTestApi('stream-idle-openai-active') + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '500' + const startedAt = Date.now() + const encoder = new TextEncoder() + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-active', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [ + { + index: 0, + delta: { role: 'assistant', content: 'hel' }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-active', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [ + { + index: 0, + delta: { content: 'lo' }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-active', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'stop', + }, + ], + }, + ]) + let emitTimer: ReturnType | undefined + + globalThis.fetch = asMockFetch(mock(async () => + new Response( + new ReadableStream({ + start(controller) { + let index = 0 + const emit = () => { + emitTimer = undefined + const chunk = chunks[index++] + if (chunk === undefined) { + controller.close() + return + } + controller.enqueue(encoder.encode(chunk)) + emitTimer = setTimeout(emit, 200) + } + emit() + }, + cancel() { + if (emitTimer !== undefined) { + clearTimeout(emitTimer) + emitTimer = undefined + } + }, + }), + { + headers: { + 'Content-Type': 'text/event-stream', + }, + }, + ))) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'glm-5.2', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + + const textDeltas: string[] = [] + for await (const event of result.data) { + const streamDelta = (event as { delta?: { type?: string; text?: string } }).delta + if ( + streamDelta?.type === 'text_delta' && + typeof streamDelta.text === 'string' + ) { + textDeltas.push(streamDelta.text) + } + } + + expect(Date.now() - startedAt).toBeGreaterThan(500) + expect(textDeltas.join('')).toBe('hello') +}) + + +// Extraction seam: Gemini stream conversion | native Ollama stream adaptation. + + +test('keeps max_completion_tokens for non-local non-github providers', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + + globalThis.fetch = (async (_input, init) => { + const body = JSON.parse(String(init?.body)) + expect(body.max_completion_tokens).toBe(64) + expect(body.max_tokens).toBeUndefined() + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-4o', + choices: [ + { + message: { + role: 'assistant', + content: 'hello', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 5, + completion_tokens: 1, + total_tokens: 6, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) +}) + +test('uses route-specific credential env vars for descriptor-backed openai-compatible routes', async () => { + let capturedHeaders: Headers | undefined + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' + process.env.OPENROUTER_API_KEY = 'or-route-key' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'openai/gpt-5-mini', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 5, + completion_tokens: 1, + total_tokens: 6, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'openai/gpt-5-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + expect(capturedHeaders?.get('authorization')).toBe('Bearer or-route-key') +}) + +test('OpenGateway MiMo replays real reasoning_content without adding empty fallback', async () => { + process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-opengateway-mimo', + model: 'mimo-v2.5-pro', + choices: [ + { + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'mimo-v2.5-pro', + messages: [ + { role: 'user', content: 'Use an agent' }, + { + role: 'assistant', + content: [ + { + type: 'thinking', + thinking: 'Need to inspect code with an agent.', + }, + { + type: 'tool_use', + id: 'call_agent_1', + name: 'Agent', + input: { + description: 'Inspect code', + prompt: 'Look at the relevant code', + subagent_type: 'general-purpose', + }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_agent_1', + content: 'Agent finished', + }, + ], + }, + ], + max_tokens: 64, + stream: false, + }) + + const assistantWithToolCall = (requestBody?.messages as Array>).find( + message => Array.isArray(message.tool_calls), + ) + + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to inspect code with an agent.', + ) + expect(requestBody).not.toHaveProperty('store') +}) + +test('Xiaomi MiMo replays real reasoning_content without adding empty fallback', async () => { + process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + process.env.MIMO_API_KEY = 'mimo-test-key' + delete process.env.OPENAI_API_KEY + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-mimo', + model: 'mimo-v2.5-pro', + choices: [ + { + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'mimo-v2.5-pro', + messages: [ + { role: 'user', content: 'Use an agent' }, + { + role: 'assistant', + content: [ + { + type: 'thinking', + thinking: 'Need to inspect code with an agent.', + }, + { + type: 'tool_use', + id: 'call_agent_1', + name: 'Agent', + input: { + description: 'Inspect code', + prompt: 'Look at the relevant code', + subagent_type: 'general-purpose', + }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_agent_1', + content: 'Agent finished', + }, + ], + }, + ], + max_tokens: 64, + stream: false, + }) + + const assistantWithToolCall = (requestBody?.messages as Array>).find( + message => Array.isArray(message.tool_calls), + ) + + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to inspect code with an agent.', + ) + expect(requestBody).not.toHaveProperty('store') +}) + +test('OpenGateway MiMo does not synthesize empty reasoning_content when missing', async () => { + process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-opengateway-mimo', + model: 'mimo-v2.5-pro', + choices: [ + { + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'mimo-v2.5-pro', + messages: [ + { role: 'user', content: 'Use an agent' }, + { + role: 'assistant', + content: [ + { + type: 'tool_use', + id: 'call_agent_1', + name: 'Agent', + input: { + description: 'Inspect code', + prompt: 'Look at the relevant code', + subagent_type: 'general-purpose', + }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_agent_1', + content: 'Agent finished', + }, + ], + }, + ], + max_tokens: 64, + stream: false, + }) + + const assistantWithToolCall = (requestBody?.messages as Array>).find( + message => Array.isArray(message.tool_calls), + ) + + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall).not.toHaveProperty('reasoning_content') + expect(requestBody).not.toHaveProperty('store') +}) + +test('strips unsupported stream_options for Xiaomi MiMo streams', async () => { + process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + process.env.MIMO_API_KEY = 'mimo-test-key' + delete process.env.OPENAI_API_KEY + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + + return makeSseResponse( + makeStreamChunks([ + { + id: 'chatcmpl-mimo', + object: 'chat.completion.chunk', + model: 'mimo-v2.5-pro', + choices: [ + { + index: 0, + delta: { role: 'assistant', content: 'done' }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-mimo', + object: 'chat.completion.chunk', + model: 'mimo-v2.5-pro', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'stop', + }, + ], + }, + ]), + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'mimo-v2.5-pro', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + + expect(requestBody).toMatchObject({ + stream: true, + max_completion_tokens: 64, + }) + expect(requestBody).not.toHaveProperty('stream_options') + expect(requestBody).not.toHaveProperty('store') +}) + +test('uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests', async () => { + let capturedAuthorization: string | null = null + let capturedProject: string | null = null + let requestUrl: string | undefined + + process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.GEMINI_AUTH_MODE = 'access-token' + process.env.GEMINI_ACCESS_TOKEN = 'gemini-access-token' + process.env.GOOGLE_CLOUD_PROJECT = 'gemini-project' + process.env.GEMINI_BASE_URL = + 'https://generativelanguage.googleapis.com/v1beta/openai' + process.env.GEMINI_MODEL = 'gemini-2.0-flash' + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_KEY + delete process.env.GEMINI_API_KEY + delete process.env.GOOGLE_API_KEY + + globalThis.fetch = (async (input, init) => { + requestUrl = typeof input === 'string' ? input : input.url + const headers = init?.headers as Record | undefined + capturedAuthorization = + headers?.Authorization ?? headers?.authorization ?? null + capturedProject = + headers?.['x-goog-user-project'] ?? + headers?.['X-Goog-User-Project'] ?? + null + + return new Response( + JSON.stringify({ + id: 'chatcmpl-gemini', + model: 'gemini-2.0-flash', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 3, + completion_tokens: 1, + total_tokens: 4, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gemini-2.0-flash', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(requestUrl).toBe( + 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', + ) + // Explicit type argument: TS narrows the closure-assigned variables to + // their `null` initializer at this point (microsoft/TypeScript#9998). + expect(capturedAuthorization).toBe('Bearer gemini-access-token') + expect(capturedProject).toBe('gemini-project') +}) + +test('uses NVIDIA_API_KEY for NVIDIA NIM requests without OPENAI_API_KEY', async () => { + let capturedAuthorization: string | null = null + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.NVIDIA_NIM = '1' + process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' + process.env.OPENAI_MODEL = 'nvidia/llama-3.1-nemotron-70b-instruct' + process.env.NVIDIA_API_KEY = 'nvidia-live-key' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + capturedAuthorization = + headers?.Authorization ?? headers?.authorization ?? null + + return new Response( + JSON.stringify({ + id: 'chatcmpl-nvidia', + model: 'nvidia/llama-3.1-nemotron-70b-instruct', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 3, + completion_tokens: 1, + total_tokens: 4, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'nvidia/llama-3.1-nemotron-70b-instruct', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedAuthorization).toBe('Bearer nvidia-live-key') +}) + +test('does not use stale NVIDIA_API_KEY for non-NVIDIA OpenAI-compatible routes', async () => { + let capturedAuthorization: string | null = null + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.NVIDIA_NIM = '1' + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' + process.env.OPENAI_MODEL = 'openai/gpt-5-mini' + process.env.NVIDIA_API_KEY = 'nvidia-live-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENROUTER_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + capturedAuthorization = + headers?.Authorization ?? headers?.authorization ?? null + + return new Response( + JSON.stringify({ + id: 'chatcmpl-openrouter', + model: 'openai/gpt-5-mini', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'openai/gpt-5-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedAuthorization).toBeNull() +}) + +test('does not use MINIMAX_API_KEY for non-MiniMax OpenAI-compatible routes', async () => { + let capturedAuthorization: string | null = null + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' + process.env.OPENAI_MODEL = 'openai/gpt-5-mini' + process.env.MINIMAX_API_KEY = 'minimax-live-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENROUTER_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + capturedAuthorization = + headers?.Authorization ?? headers?.authorization ?? null + + return new Response( + JSON.stringify({ + id: 'chatcmpl-openrouter', + model: 'openai/gpt-5-mini', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'openai/gpt-5-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedAuthorization).toBeNull() +}) + +test('xiaomi mimo route uses api-key auth header and max_completion_tokens', async () => { + let capturedHeaders: Record | undefined + let capturedBody: Record | undefined + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + process.env.MIMO_API_KEY = 'mimo-live-key' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = init?.headers as Record + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'chatcmpl-mimo', + model: 'mimo-v2.5-pro', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'mimo-v2.5-pro', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedHeaders).toMatchObject({ 'api-key': 'mimo-live-key' }) + expect(capturedHeaders).not.toHaveProperty('Authorization') + expect(capturedBody).toMatchObject({ max_completion_tokens: 32 }) + expect(capturedBody).not.toHaveProperty('max_tokens') +}) +test('xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_effort', async () => { + let capturedHeaders: Record | undefined + let capturedBody: Record | undefined + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://token-plan-sgp.xiaomimimo.com/v1' + process.env.OPENAI_MODEL = 'mimo-v2.5-pro' + process.env.MIMO_API_KEY = 'mimo-token-key' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = init?.headers as Record + capturedBody = JSON.parse(String(init?.body)) as Record + + return makeChatCompletionResponse('mimo-v2.5-pro') + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + reasoningEffort: 'high', + }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'mimo-v2.5-pro', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedHeaders).toMatchObject({ 'api-key': 'mimo-token-key' }) + expect(capturedHeaders).not.toHaveProperty('Authorization') + expect(capturedBody).toMatchObject({ + max_completion_tokens: 32, + reasoning_effort: 'high', + }) + expect(capturedBody).not.toHaveProperty('max_tokens') + expect(capturedBody).not.toHaveProperty('store') + expect(capturedBody).not.toHaveProperty('stream_options') +}) + +test.each([ + 'minimax-m3', + 'minimax-m2.7', + 'qwen3.7-max', + 'qwen3.7-plus', + 'qwen3.6-plus', +])('opencode go %s direct env routing ignores stale custom auth and uses the Anthropic Messages request contract', async model => { + let capturedUrl = '' + let capturedHeaders: Headers | undefined + let capturedBody: Record | undefined + + process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' + delete process.env.OPENAI_API_KEY + process.env.OPENAI_MODEL = model + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENCODE_API_KEY = 'fake-opencode-key' + process.env.OPENAI_AUTH_HEADER = 'Authorization' + process.env.OPENAI_AUTH_SCHEME = 'bearer' + process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-header-value' + + globalThis.fetch = (async (input, init) => { + capturedUrl = String(input) + capturedHeaders = new Headers(init?.headers) + capturedBody = JSON.parse(String(init?.body)) as Record + + return new Response( + JSON.stringify({ + id: 'msg_opencode_go', + type: 'message', + role: 'assistant', + model, + content: [{ type: 'text', text: 'ok' }], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 1, + output_tokens: 1, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model, + system: 'test system', + messages: [ + { + role: 'user', + content: [{ type: 'text', text: 'hello' }], + }, + ], + max_tokens: 32, + stream: false, + }) + + expect(capturedUrl).toBe('https://opencode.ai/zen/go/v1/messages') + expect(capturedHeaders?.get('x-api-key')).toBe('fake-opencode-key') + expect(capturedHeaders?.get('authorization')).toBeNull() + expect(capturedBody).toEqual({ + model, + messages: [ + { + role: 'user', + content: [{ type: 'text', text: 'hello' }], + }, + ], + max_tokens: 32, + stream: false, + system: 'test system', + }) + expect(capturedBody).not.toHaveProperty('max_completion_tokens') + expect(capturedBody).not.toHaveProperty('store') +}) + +test('opencode go messages endpoint rotates raw x-api-key credentials after rate-limit failure', async () => { + const capturedUrls: string[] = [] + const capturedKeys: Array = [] + + process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_API_KEYS + process.env.OPENAI_MODEL = 'minimax-m3' + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENCODE_API_KEY = 'fake-opencode-a,fake-opencode-b' + + globalThis.fetch = (async (input, init) => { + const headers = new Headers(init?.headers) + capturedUrls.push(String(input)) + capturedKeys.push(headers.get('x-api-key')) + + if (capturedKeys.length === 1) { + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + } + + return new Response( + JSON.stringify({ + id: 'msg_opencode_go_retry', + type: 'message', + role: 'assistant', + model: 'minimax-m3', + content: [{ type: 'text', text: 'ok' }], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 1, + output_tokens: 1, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'minimax-m3', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedUrls).toEqual([ + 'https://opencode.ai/zen/go/v1/messages', + 'https://opencode.ai/zen/go/v1/messages', + ]) + expect(capturedKeys).toEqual(['fake-opencode-a', 'fake-opencode-b']) +}) + +test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY as bearer auth despite stale generic base URL', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('https://opengateway.gitlawb.com/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-ogw-key') +}) + +test('gitlawb opengateway provider flag accepts OPENAI_API_KEY compatibility fallback', async () => { + delete process.env.OPENAI_BASE_URL + delete process.env.OPENGATEWAY_API_KEY + process.env.OPENAI_API_KEY = 'fake-openai-fallback' + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.authorization).toBe('Bearer fake-openai-fallback') +}) + +test('gitlawb opengateway provider flag sends OPENAI_API_KEY fallback despite stale generic base URL', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_API_KEY = 'fake-openai-fallback' + delete process.env.OPENGATEWAY_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('https://opengateway.gitlawb.com/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-openai-fallback') +}) + +test('gitlawb opengateway provider flag trims OPENGATEWAY_API_KEY before bearer auth', async () => { + process.env.OPENGATEWAY_API_KEY = ' fake-ogw-key ' + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.authorization).toBe('Bearer fake-ogw-key') +}) + +test('gitlawb opengateway provider flag ignores blank OPENGATEWAY_API_KEY and uses OPENAI_API_KEY fallback', async () => { + process.env.OPENGATEWAY_API_KEY = ' ' + process.env.OPENAI_API_KEY = 'fake-openai-fallback' + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.authorization).toBe('Bearer fake-openai-fallback') +}) + +test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to OPENGATEWAY_BASE_URL override', async () => { + process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' + process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-ogw-key') +}) + +test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to custom OPENAI_BASE_URL fallback', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:8181/v1' + process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' + delete process.env.OPENGATEWAY_BASE_URL + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-ogw-key') +}) + +test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEY for custom base URL', async () => { + process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' + process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' + process.env.OPENAI_API_KEY = 'fake-generic-openai-key' + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-ogw-key') +}) + +test('gitlawb opengateway stored provider profile key becomes bearer auth', async () => { + delete process.env.OPENAI_API_KEY + delete process.env.OPENGATEWAY_API_KEY + + applyProviderProfileToProcessEnv({ + id: 'stored-opengateway', + provider: 'gitlawb-opengateway', + name: 'Gitlawb Opengateway', + baseUrl: 'https://opengateway.gitlawb.com/v1', + model: 'mimo-v2.5-pro', + apiKey: 'fake-profile-key', + }) + + const captured = await captureChatCompletionRequest() + + expect(captured.authorization).toBe('Bearer fake-profile-key') +}) + +test('openai route still sends OPENAI_API_KEY as bearer auth', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEY = 'fake-openai-key' + delete process.env.OPENGATEWAY_API_KEY + + const captured = await captureChatCompletionRequest('gpt-5.5') + + expect(captured.authorization).toBe('Bearer fake-openai-key') +}) +test('does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes', async () => { + let capturedAuthorization: string | null = null + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' + process.env.OPENAI_MODEL = 'openai/gpt-5-mini' + process.env.BNKR_API_KEY = 'bankr-live-key' + delete process.env.OPENAI_API_KEY + delete process.env.OPENROUTER_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + capturedAuthorization = + headers?.Authorization ?? headers?.authorization ?? null + + return new Response( + JSON.stringify({ + id: 'chatcmpl-openrouter', + model: 'openai/gpt-5-mini', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'openai/gpt-5-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(capturedAuthorization).toBeNull() +}) + +// Extraction seam: provider signature metadata | raw streaming tool fallback. + +// Extraction seam: streaming conversion | non-streaming response conversion. + +test('normalizes plain string Bash tool arguments from OpenAI-compatible responses', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [ + { + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: 'pwd', + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: false, + }) as { + stop_reason?: string + content?: Array> + } + + expect(message.stop_reason).toBe('tool_use') + expect(message.content).toEqual([ + { + type: 'tool_use', + id: 'function-call-1', + name: 'Bash', + input: { command: 'pwd' }, + }, + ]) +}) + +test('normalizes Bash tool arguments that are valid JSON strings', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [ + { + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '"pwd"', + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: false, + }) as { + content?: Array> + } + + expect(message.content).toEqual([ + { + type: 'tool_use', + id: 'function-call-1', + name: 'Bash', + input: { command: 'pwd' }, + }, + ]) +}) + +test.each([ + ['false', false], + ['null', null], + ['[]', []], +])( + 'preserves malformed Bash JSON literals as parsed values in non-streaming responses: %s', + async (argumentsValue, expectedInput) => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [ + { + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: argumentsValue, + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: false, + }) as { + content?: Array> + } + + expect(message.content).toEqual([ + { + type: 'tool_use', + id: 'function-call-1', + name: 'Bash', + input: expectedInput, + }, + ]) + }, +) + +test('keeps terminal empty Bash tool arguments invalid in non-streaming responses', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [ + { + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '', + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: false, + }) as { + content?: Array> + } + + expect(message.content).toEqual([ + { + type: 'tool_use', + id: 'function-call-1', + name: 'Bash', + input: {}, + }, + ]) +}) + +// Extraction seam: completed tool parsing | streamed tool normalization. + +// Extraction seam: streamed tool normalization | schema and tool conversion. + +test('preserves raw input for unknown plain string tool arguments', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [ + { + id: 'function-call-1', + type: 'function', + function: { + name: 'UnknownTool', + arguments: 'pwd', + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use tool' }], + max_tokens: 64, + stream: false, + }) as { + content?: Array> + } + + expect(message.content).toEqual([ + { + type: 'tool_use', + id: 'function-call-1', + name: 'UnknownTool', + input: {}, + }, + ]) +}) + +test('preserves parsed string input for unknown JSON string tool arguments', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [ + { + id: 'function-call-1', + type: 'function', + function: { + name: 'UnknownTool', + arguments: '"pwd"', + }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use tool' }], + max_tokens: 64, + stream: false, + }) as { + content?: Array> + } + + expect(message.content).toEqual([ + { + type: 'tool_use', + id: 'function-call-1', + name: 'UnknownTool', + input: 'pwd', + }, + ]) +}) + +test('non-streaming: preserves response body when usage parsing fails', async () => { + const json = JSON as unknown as { parse: typeof JSON.parse } + const originalJSONParse = json.parse + const responseBody = JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5', + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + }, + }) + let usageParseFailed = false + + // Throw only for the usage-extraction parse of the response body. + // A global "throw once" mock is unreliable here: Bun's native + // Response.json() does not go through JS-level JSON.parse, so the + // second parse the original test relied on never happens (parseCalls + // stays at 1 and `toBeGreaterThan(1)` fails). Scoping the failure to + // the response body targets the _doRequest parse without breaking + // unrelated JSON.parse calls in the request pipeline, and works in + // both Bun (native Response.json) and Node (undici, which does call + // JSON.parse — guarded by `usageParseFailed` so it won't throw again). + json.parse = ((text: string, reviver?: Parameters[1]) => { + if (!usageParseFailed && text === responseBody) { + usageParseFailed = true + throw new Error('simulated usage parse failure') + } + return originalJSONParse(text, reviver) + }) as typeof JSON.parse + + try { + globalThis.fetch = (async () => { + return new Response(responseBody, { + headers: { + 'Content-Type': 'application/json', + }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const result = (await client.beta.messages.create({ + model: 'glm-5', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + })) as { content: Array> } + + // Usage extraction threw, but the recreated Response still holds the + // body so downstream response.json() can read it. + expect(usageParseFailed).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'ok' }]) + } finally { + json.parse = originalJSONParse + } +}) + +test('non-streaming: preserves response.url routing metadata after body read', async () => { + // _doRequest reads the body for usage extraction and recreates the + // Response with new Response(bodyText, ...). That drops response.url to + // "", which breaks create()'s /responses, /messages, and Gemini routing. + // This test pins an Anthropic-shaped body behind a /messages URL: if url + // is preserved, create() passes the body through unchanged; if url is + // lost, it falls through to _convertNonStreamingResponse and the + // Anthropic-only fields (stop_reason, input_tokens) surface as wrong + // output or missing content. + const anthropicBody = JSON.stringify({ + id: 'msg_1', + type: 'message', + role: 'assistant', + content: [{ type: 'text', text: 'passthrough ok' }], + model: 'claude-3', + stop_reason: 'end_turn', + usage: { input_tokens: 10, output_tokens: 20 }, + }) + + globalThis.fetch = (async () => { + const r = new Response(anthropicBody, { + headers: { 'Content-Type': 'application/json' }, + }) + // fetch() sets .url from the request; new Response() cannot. Simulate + // the fetch-attached URL so create()'s routing can see /messages. + Object.defineProperty(r, 'url', { + value: 'https://api.anthropic-shaped.example.com/v1/messages', + configurable: true, + }) + return r + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const result = (await client.beta.messages.create({ + model: 'glm-5', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + })) as { content: Array> } + + // /messages passthrough returns the Anthropic body verbatim. If url were + // lost, _convertNonStreamingResponse would try to read OpenAI choices[] + // and content would not match. + expect(result.content).toEqual([{ type: 'text', text: 'passthrough ok' }]) +}) + +// Extraction seam: non-streaming response conversion | streaming event conversion. + +// Extraction boundary: response conversion | executor network behavior. +// The executor suite owns the contiguous network-classification block below. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: executor network behavior | native Ollama routing. +// Native Ollama endpoint selection remains an adapter/facade integration concern. +// Keep this marker stable for independent adjacent test migrations. +test('keeps remote Ollama-named gateways on chat completions', async () => { + process.env.OPENAI_BASE_URL = 'https://ollama-gateway.example.com/v1' + + const requestUrls: string[] = [] + globalThis.fetch = (async (input, init) => { + const url = typeof input === 'string' ? input : input.url + requestUrls.push(url) + const body = JSON.parse(String(init?.body)) as Record + expect(body.max_tokens).toBe(64) + expect(body.options).toBeUndefined() + + return makeChatCompletionResponse('llama3.1:8b') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'llama3.1:8b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).resolves.toBeDefined() + + expect(requestUrls).toEqual([ + 'https://ollama-gateway.example.com/v1/chat/completions', + ]) +}) + +test('keeps HTTPS localhost Ollama-port proxies on chat completions', async () => { + process.env.OPENAI_BASE_URL = 'https://localhost:11434/v1' + + const requestUrls: string[] = [] + globalThis.fetch = (async (input, init) => { + const url = typeof input === 'string' ? input : input.url + requestUrls.push(url) + const body = JSON.parse(String(init?.body)) as Record + expect(body.max_tokens).toBe(64) + expect(body.options).toBeUndefined() + + return makeChatCompletionResponse('llama3.1:8b') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'llama3.1:8b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).resolves.toBeDefined() + + expect(requestUrls).toEqual([ + 'https://localhost:11434/v1/chat/completions', + ]) +}) + +// Extraction boundary: executor tool self-healing | message conversion. +// Message-history normalization below belongs to the message converter. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: native Ollama routing | executor tool self-healing. +// The single retry test below moves with request execution. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: executor tool self-healing | message conversion. +// Message-history normalization below belongs to the message converter. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: executor tool self-healing | message/provider shaping. +// Provider request shaping below is not owned by the executor. +// Keep this marker stable for independent adjacent test migrations. +test('Moonshot: uses max_tokens (not max_completion_tokens) and strips store', async () => { + process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' + process.env.OPENAI_API_KEY = 'sk-moonshot-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'kimi-k2.6', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'kimi-k2.6', + system: 'you are kimi', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 256, + stream: false, + }) + + expect(requestBody?.max_tokens).toBe(256) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + +test('Cerebras: strips unsupported store on chat_completions (#1023)', async () => { + process.env.OPENAI_BASE_URL = 'https://api.cerebras.ai/v1' + process.env.OPENAI_API_KEY = 'csk-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'llama3.1-8b', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'llama3.1-8b', + system: 'you are cerebras', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.store).toBeUndefined() +}) + +test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_completions (#672)', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:8000/v1' + process.env.OPENAI_API_KEY = 'sk-local' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'qwen-3.5-27b', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'qwen-3.5-27b', + system: 'you are local', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.store).toBeUndefined() +}) + +test('Mistral: strips unsupported store on chat_completions (#739)', async () => { + process.env.OPENAI_BASE_URL = 'https://api.mistral.ai/v1' + process.env.OPENAI_API_KEY = 'mistral-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'codestral-2508', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'codestral-2508', + system: 'you are mistral', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.store).toBeUndefined() +}) + +test('Mistral host fallback: strips store on an unresolved Mistral-host route (#739)', async () => { + // `api.mistral.ai/v1` resolves to the Mistral descriptor route, whose + // removeBodyFields already strips `store` — so the test above passes even + // without the hasMistralApiHost fallback. This case pins the fallback's real + // value: a Mistral-host proxy (`proxy.mistral.ai`) that does NOT resolve to a + // descriptor route (resolveRouteIdFromBaseUrl returns null, no + // removeBodyFields), so `store` is stripped *only* by hasMistralApiHost. + process.env.OPENAI_BASE_URL = 'https://proxy.mistral.ai/v1' + process.env.OPENAI_API_KEY = 'mistral-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'codestral-2508', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'codestral-2508', + system: 'you are mistral', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + // The shim sets `store: false` on every chat_completions body; without the + // fallback this unresolved route would forward it and hit Mistral's 422. + expect(requestBody?.store).toBeUndefined() + // #739's Mistral 422 rejects `max_completion_tokens` as well — the host + // fallback must also map it to `max_tokens` on the unresolved route, since + // the generic config leaves the `max_completion_tokens` default. + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.max_tokens).toBe(64) +}) + + +test('Groq: keeps max_completion_tokens and strips unsupported store', async () => { + process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' + process.env.OPENAI_API_KEY = 'gsk-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'llama-3.3-70b-versatile', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'llama-3.3-70b-versatile', + system: 'you are groq', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 256, + stream: false, + }) + + expect(requestBody?.max_completion_tokens).toBe(256) + expect(requestBody?.max_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + + +test('Groq: strips reasoning_effort even when compat inference matches the model', async () => { + process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' + process.env.OPENAI_API_KEY = 'gsk-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-r1-distill-llama-70b', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ reasoningEffort: 'xhigh' }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-r1-distill-llama-70b', + system: 'you are groq', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 256, + stream: false, + thinking: { type: 'enabled' }, + }) + + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) +test('Moonshot: echoes reasoning_content on assistant tool-call messages', async () => { + // Regression for: "API Error: 400 {"error":{"message":"thinking is enabled + // but reasoning_content is missing in assistant tool call message at index + // N"}}" when the agent sends a prior-turn assistant response back to Kimi. + // The thinking block captured from the inbound response must round-trip + // as reasoning_content on the outgoing echoed assistant message. + process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' + process.env.OPENAI_API_KEY = 'sk-moonshot-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'kimi-k2.6', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'kimi-k2.6', + system: 'you are kimi', + messages: [ + { role: 'user', content: 'check the logs' }, + { + role: 'assistant', + content: [ + { + type: 'thinking', + thinking: 'Need to inspect logs via Bash; running a cat.', + }, + { type: 'text', text: "I'll inspect the logs." }, + { + type: 'tool_use', + id: 'call_bash_1', + name: 'Bash', + input: { command: 'cat /tmp/app.log' }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_bash_1', + content: 'log line 1\nlog line 2', + }, + ], + }, + ], + max_tokens: 256, + stream: false, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to inspect logs via Bash; running a cat.', + ) +}) + +test('DeepSeek echoes reasoning_content on assistant tool-call messages', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-flash', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-flash', + system: 'test', + messages: [ + { role: 'user', content: 'hi' }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'thought' }, + { type: 'text', text: 'hello' }, + { + type: 'tool_use', + id: 'call_1', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, + ], + }, + ], + max_tokens: 32, + stream: false, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe('thought') +}) + +test('generic OpenAI-compatible providers do not echo reasoning_content on assistant tool-call messages', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_API_KEY = 'sk-openai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-4o', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'gpt-4o', + system: 'test', + messages: [ + { role: 'user', content: 'hi' }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'thought' }, + { type: 'text', text: 'hello' }, + { + type: 'tool_use', + id: 'call_1', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, + ], + }, + ], + max_tokens: 32, + stream: false, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBeUndefined() +}) + +test('gateway-routed DeepSeek models inherit descriptor-backed reasoning and token shaping', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' + process.env.OPENAI_API_KEY = 'sk-openrouter-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek/deepseek-reasoner', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + reasoningEffort: 'xhigh', + }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek/deepseek-reasoner', + system: 'test', + messages: [ + { role: 'user', content: 'hi' }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'thought' }, + { type: 'text', text: 'hello' }, + { + type: 'tool_use', + id: 'call_1', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, + ], + }, + ], + max_tokens: 64, + stream: false, + thinking: { type: 'enabled' }, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + message => message.role === 'assistant' && Array.isArray(message.tool_calls), + ) + + expect(assistantWithToolCall?.reasoning_content).toBe('thought') + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe('max') + expect(requestBody?.max_tokens).toBe(64) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + +test('Moonshot: cn host is also detected', async () => { + process.env.OPENAI_BASE_URL = 'https://api.moonshot.cn/v1' + process.env.OPENAI_API_KEY = 'sk-moonshot-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'kimi-k2.6', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'kimi-k2.6', + system: 'you are kimi', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 256, + stream: false, + }) + + expect(requestBody?.store).toBeUndefined() +}) + +test('Kimi Code endpoint inherits Moonshot max_tokens/store compatibility', async () => { + process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1' + process.env.OPENAI_API_KEY = 'sk-kimi-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'kimi-for-coding', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'kimi-for-coding', + system: 'you are kimi code', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 256, + stream: false, + }) + + expect(requestBody?.max_tokens).toBe(256) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + +test('Kimi Code endpoint echoes reasoning_content on assistant tool-call messages', async () => { + process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1' + process.env.OPENAI_API_KEY = 'sk-kimi-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'kimi-for-coding', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'kimi-for-coding', + system: 'you are kimi code', + messages: [ + { role: 'user', content: 'check the logs' }, + { + role: 'assistant', + content: [ + { + type: 'thinking', + thinking: 'Need to inspect logs via Bash; running a cat.', + }, + { type: 'text', text: "I'll inspect the logs." }, + { + type: 'tool_use', + id: 'call_bash_1', + name: 'Bash', + input: { command: 'cat /tmp/app.log' }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_bash_1', + content: 'log line 1\nlog line 2', + }, + ], + }, + ], + max_tokens: 256, + stream: false, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to inspect logs via Bash; running a cat.', + ) +}) + +test('DeepSeek sends thinking toggle and normalized reasoning effort', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-pro', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + reasoningEffort: 'xhigh', + }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-pro', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + thinking: { type: 'enabled' }, + }) + + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe('max') + expect(requestBody?.max_tokens).toBe(64) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + +test('DeepSeek omits thinking controls when the Anthropic-side request does not set them', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-flash', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-flash', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 32, + stream: false, + }) + + expect(requestBody?.thinking).toBeUndefined() + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + +test('DeepSeek forwards an explicit thinking disable toggle for V4 models', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-flash', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-flash', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 32, + stream: false, + thinking: { type: 'disabled' }, + }) + + expect(requestBody?.thinking).toEqual({ type: 'disabled' }) + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + + +test('Z.AI: uses max_tokens (not max_completion_tokens) and strips store', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'GLM-5.1', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'GLM-5.1', + system: 'you are glm', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 256, + stream: false, + }) + + expect(requestBody?.max_tokens).toBe(256) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + +test('Z.AI: thinking mode enabled when requested', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'GLM-5.1', + choices: [ + { + message: { + role: 'assistant', + content: null, + reasoning_content: 'Let me think...', + }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'GLM-5.1', + system: 'you are glm', + messages: [{ role: 'user', content: 'think hard' }], + max_tokens: 1024, + stream: false, + thinking: { type: 'enabled', budget_tokens: 1024 }, + }) + + expect((requestBody?.thinking as Record)?.type).toBe('enabled') + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.max_tokens).toBe(1024) +}) + +test('Z.AI GLM-5.2: default request relies on provider thinking defaults', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5.2', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.model).toBe('glm-5.2') + expect(requestBody?.thinking).toBeUndefined() + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + +test('Z.AI GLM-5.2: user-selected xhigh effort maps to provider max effort', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5.2', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + reasoningEffort: 'xhigh', + }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.model).toBe('glm-5.2') + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe('max') +}) + +test.each([ + ['glm-5.2?reasoning=low', 'high'], + ['glm-5.2?reasoning=medium', 'high'], + ['glm-5.2?reasoning=high', 'high'], + ['glm-5.2?reasoning=xhigh', 'max'], + ['openrouter/zhipu/glm-5.2?reasoning=low', 'high'], + ['openrouter/zhipu/glm-5.2?reasoning=medium', 'high'], + ['openrouter/zhipu/glm-5.2?reasoning=high', 'high'], + ['openrouter/zhipu/glm-5.2?reasoning=xhigh', 'max'], +] as const)('Z.AI GLM-5.2: %s enables mapped reasoning effort', async (model, effort) => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + const expectedModel = model.split('?')[0]; + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: expectedModel, + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.model).toBe(expectedModel) + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe(effort) +}) + +test.each([ + 'GLM-5.1?reasoning=high', + 'GLM-4.5-Air?reasoning=high', +] as const)('Z.AI GLM: %s does not receive GLM-5.2-only reasoning_effort', async model => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model, + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.model).toBe(model.split('?', 1)[0]) + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + +test('Z.AI GLM-5.2: model-query thinking disable omits reasoning effort', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5.2', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2?thinking=disabled&reasoning=xhigh', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) + + expect(requestBody?.model).toBe('glm-5.2') + expect(requestBody?.thinking).toEqual({ type: 'disabled' }) + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + +test('Z.AI GLM-5.2: per-turn thinking overrides model-query default', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5.2', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2?thinking=disabled&reasoning=high', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + thinking: { type: 'enabled' }, + }) + + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe('high') +}) + +// Extraction boundary: provider reasoning compatibility | tool-stream routing. +// The gateway emission regression below remains provider/request-shaping coverage. +// Keep this marker stable for independent adjacent test migrations. +// Regression test for #1950: GLM-5.2 served through NVIDIA NIM +// (`integrate.api.nvidia.com`) must never receive the Z.AI-proprietary +// `tool_stream` parameter. Streaming tool calls are simply not streamed on +// this gateway; sending the parameter aborts the request with +// `400 Unsupported parameter(s): tool_stream`. +test('NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream (regression #1950)', async () => { + process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' + process.env.NVIDIA_API_KEY = 'nvapi-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'z-ai/glm-5.2', + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'z-ai/glm-5.2', + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'z-ai/glm-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ], + max_tokens: 64, + stream: true, + }) + + // tool_stream is a Z.AI-only streaming extension; NVIDIA NIM rejects it with + // `400 Unsupported parameter(s): tool_stream`. Streaming tool calls simply + // aren't streamed on this gateway. + expect(requestBody?.tool_stream).toBeUndefined() +}) + +// Extraction boundary: executor tool-stream retry | provider tool-stream shaping. +// Provider emission rules below remain with compatibility/request planning. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: provider tool-stream shaping | executor tool-stream retry. +// The three retry-state tests below move together with request execution. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: executor tool-stream retry | provider tool-stream shaping. +// Provider emission rules below remain with compatibility/request planning. +// Keep this marker stable for independent adjacent test migrations. +test('Z.AI GLM-5.2: streaming requests with tools send tool_stream', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ], + max_tokens: 64, + stream: true, + }) + + expect(requestBody?.tool_stream).toBe(true) +}) + +test('Hicap GLM-5.2: uses Z.AI-compatible request shaping', async () => { + process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' + process.env.HICAP_API_KEY = 'sk-hicap-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ reasoningEffort: 'xhigh' }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'GLM-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ], + max_tokens: 64, + stream: true, + }) + + expect(requestBody?.model).toBe('glm-5.2') + expect(requestBody?.store).toBeUndefined() + expect(requestBody?.max_tokens).toBe(64) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe('max') + expect(requestBody?.tool_stream).toBe(true) +}) +test('Z.AI GLM-5.2: remote tool incompatibility does not use local toolless retry', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + const requestBodies: Array> = [] + globalThis.fetch = (async (_input, init) => { + requestBodies.push(JSON.parse(String(init?.body)) as Record) + return new Response('tool_calls are not supported', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ], + max_tokens: 64, + stream: true, + }), + ).rejects.toThrow() + + expect(requestBodies).toHaveLength(1) + expect(requestBodies[0]?.tool_stream).toBe(true) +}) + +test.each([ + ['non-streaming Z.AI request with tools', 'https://api.z.ai/api/coding/paas/v4', false, true, 'glm-5.2'], + ['streaming Z.AI request without tools', 'https://api.z.ai/api/coding/paas/v4', true, false, 'glm-5.2'], + ['streaming non-Z.AI request with tools', 'https://api.openai.com/v1', true, true, 'gpt-4o'], +] as const)('does not send tool_stream for %s', async (_name, baseUrl, stream, includeTools, model) => { + process.env.OPENAI_BASE_URL = baseUrl + process.env.OPENAI_API_KEY = 'sk-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + if (stream) { + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model, + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model, + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) + } + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model, + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hi' }], + tools: includeTools + ? [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ] + : undefined, + max_tokens: 64, + stream, + }) + + expect(requestBody?.tool_stream).toBeUndefined() +}) + +test('Z.AI GLM-5.2: preserved thinking round-trips with tool calls', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5.2', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2', + messages: [ + { role: 'user', content: 'inspect files' }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'Need to list files before answering.' }, + { + type: 'tool_use', + id: 'call_bash_1', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_bash_1', content: 'README.md' }, + ], + }, + ], + max_tokens: 64, + stream: false, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + message => message.role === 'assistant' && Array.isArray(message.tool_calls), + ) + + expect(assistantWithToolCall?.reasoning_content).toBe( + 'Need to list files before answering.', + ) + expect(assistantWithToolCall?.tool_calls).toEqual([ + { + id: 'call_bash_1', + type: 'function', + function: { + name: 'Bash', + arguments: JSON.stringify({ command: 'ls' }), + }, + }, + ]) +}) + +test('emits reasoning_effort on chat_completions when reasoningEffort is passed', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_API_KEY = 'test-key' + process.env.OPENAI_API_FORMAT = 'chat_completions' + + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-5.4', + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + reasoningEffort: 'xhigh', + }) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-5.4', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 16, + stream: false, + }) + + expect(requestBody?.reasoning_effort).toBe('xhigh') +}) + +test('omits reasoning_effort on chat_completions when no override and model has no alias default', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_API_KEY = 'test-key' + + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-4o', + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 16, + stream: false, + }) + + expect(requestBody && 'reasoning_effort' in requestBody).toBe(false) +}) + +test('emits reasoning_effort from codex alias default when no override is passed', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_API_KEY = 'test-key' + process.env.OPENAI_API_FORMAT = 'chat_completions' + + let requestBody: Record | undefined + + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-5.4', + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model: 'gpt-5.4', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 16, + stream: false, + }) + + expect(requestBody?.reasoning_effort).toBe('high') +}) + +// Extraction boundary: history pruning | executor Copilot refresh behavior. +// The contiguous Copilot authentication retry block below moves with execution. +// Keep this marker stable for independent adjacent test migrations. +// Extraction boundary: executor Copilot refresh behavior | JSON fallback conversion. +// JSON fallback response conversion below is not owned by request execution. +// Keep this marker stable for independent adjacent test migrations. +// --- JSON fallback regression tests (#1749) ------------------------------- +// Some OpenAI-compatible providers ignore `stream: true` and return a full +// `application/json` chat completion. The fallback inside +// openaiStreamToAnthropic must route that response through the same +// non-streaming converter so tool_calls, Anthropic stop reasons, array +// content, and stripping are all preserved (jatmn CHANGES_REQUESTED). + +function makeJsonChatCompletion(body: Record): Response { + return new Response(JSON.stringify(body), { + headers: { 'Content-Type': 'application/json' }, + }) +} + +async function collectFallbackEvents( + body: Record, + model = 'fake-model', +): Promise>> { + const previousFetch = globalThis.fetch + globalThis.fetch = (async () => makeJsonChatCompletion(body)) as unknown as FetchType + try { + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model, + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } + return events + } finally { + // Restore so the global fetch stub does not leak past this helper. + globalThis.fetch = previousFetch + } +} + +test('JSON fallback: preserves tool_calls as a tool_use block', async () => { + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-tool', + model: 'fake-model', + choices: [ + { + message: { + role: 'assistant', + content: null, + tool_calls: [ + { + id: 'call_1', + type: 'function', + function: { name: 'Bash', arguments: '{"command":"pwd"}' }, + }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + }) + + const toolStart = events.find( + event => + event.type === 'content_block_start' && + typeof event.content_block === 'object' && + event.content_block !== null && + (event.content_block as Record).type === 'tool_use', + ) as { content_block?: Record } | undefined + expect(toolStart?.content_block).toMatchObject({ + type: 'tool_use', + id: 'call_1', + name: 'Bash', + }) + + const inputDelta = events.find( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) as { delta?: { partial_json?: string } } | undefined + expect(JSON.parse(inputDelta?.delta?.partial_json ?? '{}')).toEqual({ + command: 'pwd', + }) + + const stopEvent = events.find(e => e.type === 'message_delta') as + | { delta?: { stop_reason?: string } } + | undefined + expect(stopEvent?.delta?.stop_reason).toBe('tool_use') +}) + +test('JSON fallback: maps finish_reason=length to max_tokens', async () => { + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-len', + model: 'fake-model', + choices: [ + { message: { role: 'assistant', content: 'partial' }, finish_reason: 'length' }, + ], + }) + const stopEvent = events.find(e => e.type === 'message_delta') as + | { delta?: { stop_reason?: string } } + | undefined + expect(stopEvent?.delta?.stop_reason).toBe('max_tokens') +}) + +test('JSON fallback: preserves OpenCode Go quota error guidance', async () => { + process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' + const previousFetch = globalThis.fetch + globalThis.fetch = (async () => + withResponseUrl( + makeJsonChatCompletion({ + error: { + type: 'FreeUsageLimitError', + message: 'free usage limit reached', + }, + }), + 'https://opencode.ai/zen/go/v1/chat/completions', + )) as unknown as FetchType + + try { + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'fake-model', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + + let caught: unknown + try { + for await (const _event of result.data) { + // Consume until the JSON error is surfaced. + } + } catch (error) { + caught = error + } + + expect(caught).toBeInstanceOf(APIError) + const apiError = caught as APIError + expect(apiError.headers?.get('x-opencode-request-url')).toBe( + 'https://opencode.ai/zen/go/v1/chat/completions', + ) + const message = getAssistantMessageFromError(apiError, 'glm-5.1') + const first = message.message.content[0] + expect(typeof first === 'object' && first && 'text' in first ? first.text : '').toBe( + OPENCODE_GO_FREE_LIMIT_ERROR_MESSAGE, + ) + } finally { + globalThis.fetch = previousFetch + } +}) + +test('JSON fallback: strips tags from emitted text', async () => { + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-think', + model: 'fake-model', + choices: [ + { + message: { role: 'assistant', content: 'private planvisible answer' }, + finish_reason: 'stop', + }, + ], + }) + const textDelta = events.find( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'text_delta', + ) as { delta?: { text?: string } } | undefined + expect(textDelta?.delta?.text).toBe('visible answer') + expect(textDelta?.delta?.text).not.toContain('private plan') +}) + +test('JSON fallback: normalizes array content into a text string', async () => { + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-array', + model: 'fake-model', + choices: [ + { + message: { + role: 'assistant', + content: [ + { type: 'text', text: 'line one' }, + { type: 'text', text: 'line two' }, + ], + }, + finish_reason: 'stop', + }, + ], + }) + const textDelta = events.find( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'text_delta', + ) as { delta?: { text?: unknown } } | undefined + expect(typeof textDelta?.delta?.text).toBe('string') + expect(textDelta?.delta?.text).toBe('line one\nline two') +}) + +test('JSON fallback façade terminates converted messages', async () => { + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-boundary', + model: 'boundary-model', + choices: [{ + message: { role: 'assistant', content: 'ok' }, + finish_reason: 'stop', + }], + }) + + expect(events.at(-1)?.type).toBe('message_stop') +}) + +test('JSON fallback: preserves HY3-looking text for non-Tencent model names', async () => { + const text = + 'TaskCreate\nsubject: merely a documentation example\n' + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-non-tencent-hy3', + model: 'other/hy3-documentation', + choices: [ + { + message: { role: 'assistant', content: text }, + finish_reason: 'stop', + }, + ], + }, 'other/hy3-documentation') + const toolStart = events.find( + event => + event.type === 'content_block_start' && + typeof event.content_block === 'object' && + event.content_block !== null && + (event.content_block as Record).type === 'tool_use', + ) + const textDelta = events.find( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'text_delta', + ) as { delta?: { text?: string } } | undefined + + expect(toolStart).toBeUndefined() + expect(textDelta?.delta?.text).toBe(text) +}) + +test('JSON fallback: empty tool_calls array does not block raw-text recovery', async () => { + // tool_calls: [] is truthy; it must be treated as "no structured tool calls" + // so the raw "Tool calls requested" recovery still runs. + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-empty-tc', + model: 'fake-model', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [], + content: + 'Tool calls requested:\n- Bash({"command":"ls"}) [id: call_empty_tc]', + }, + finish_reason: 'stop', + }, + ], + }) + const toolStart = events.find( + event => + event.type === 'content_block_start' && + typeof event.content_block === 'object' && + event.content_block !== null && + (event.content_block as Record).type === 'tool_use', + ) as { content_block?: Record } | undefined + expect(toolStart?.content_block).toMatchObject({ + type: 'tool_use', + id: 'call_empty_tc', + name: 'Bash', + }) +}) + +test('JSON fallback: empty tool_calls does not block raw-text recovery on array content', async () => { + // Companion to the string-content case above: the array-content branch must + // also treat tool_calls: [] as "no structured tool calls" so raw recovery runs. + const events = await collectFallbackEvents({ + id: 'chatcmpl-json-empty-tc-array', + model: 'fake-model', + choices: [ + { + message: { + role: 'assistant', + tool_calls: [], + content: [ + { type: 'text', text: 'Tool calls requested:' }, + { type: 'text', text: '- Bash({"command":"ls"}) [id: call_empty_tc_arr]' }, + ], + }, + finish_reason: 'stop', + }, + ], + }) + const toolStart = events.find( + event => + event.type === 'content_block_start' && + typeof event.content_block === 'object' && + event.content_block !== null && + (event.content_block as Record).type === 'tool_use', + ) as { content_block?: Record } | undefined + expect(toolStart?.content_block).toMatchObject({ + type: 'tool_use', + id: 'call_empty_tc_arr', + name: 'Bash', + }) +}) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts new file mode 100644 index 0000000000..c9723a15af --- /dev/null +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -0,0 +1,1913 @@ +import { APIError } from '@anthropic-ai/sdk' +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../../test/sharedMutationLock.js' +import { asMockFetch } from '../../../test/typedMocks.js' +import { _clearRegistryForTesting, ensureIntegrationsLoaded, registerGateway } from '../../../integrations/index.ts' +import { applyProviderFlag } from '../../../utils/providerFlag.ts' +import { applyProviderProfileToProcessEnv } from '../../../utils/providerProfiles.ts' +import { + getAssistantMessageFromError, + OPENCODE_GO_FREE_LIMIT_ERROR_MESSAGE, +} from '../errors.ts' +import { createOpenAIShimClient, hasMistralApiHost } from '../openaiShim.ts' +import { formatRetryAfterHint, sleepMs } from './requestExecutor.js' +import * as realCodexShim from '../codexShim.js' +import * as realGithubModelsCredentials from '../../../utils/githubModelsCredentials.js' + +type FetchType = typeof globalThis.fetch + +const originalEnv = { + OPENAI_BASE_URL: process.env.OPENAI_BASE_URL, + OPENAI_API_BASE: process.env.OPENAI_API_BASE, + OPENAI_API_KEY: process.env.OPENAI_API_KEY, + OPENAI_API_KEYS: process.env.OPENAI_API_KEYS, + OPENAI_MODEL: process.env.OPENAI_MODEL, + OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT, + OPENAI_AUTH_HEADER: process.env.OPENAI_AUTH_HEADER, + OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, + OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE, + CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, + GITHUB_COPILOT_KEY: process.env.GITHUB_COPILOT_KEY, + GITHUB_ENTERPRISE_URL: process.env.GITHUB_ENTERPRISE_URL, + GITHUB_TOKEN: process.env.GITHUB_TOKEN, + GH_TOKEN: process.env.GH_TOKEN, + CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, + CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, + GEMINI_API_KEY: process.env.GEMINI_API_KEY, + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY, + GEMINI_ACCESS_TOKEN: process.env.GEMINI_ACCESS_TOKEN, + GEMINI_AUTH_MODE: process.env.GEMINI_AUTH_MODE, + GEMINI_BASE_URL: process.env.GEMINI_BASE_URL, + GEMINI_MODEL: process.env.GEMINI_MODEL, + GOOGLE_CLOUD_PROJECT: process.env.GOOGLE_CLOUD_PROJECT, + ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS, + NVIDIA_API_KEY: process.env.NVIDIA_API_KEY, + NVIDIA_NIM: process.env.NVIDIA_NIM, + MINIMAX_API_KEY: process.env.MINIMAX_API_KEY, + BNKR_API_KEY: process.env.BNKR_API_KEY, + BANKR_BASE_URL: process.env.BANKR_BASE_URL, + BANKR_MODEL: process.env.BANKR_MODEL, + OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, + DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY, + MIMO_API_KEY: process.env.MIMO_API_KEY, + OPENGATEWAY_API_KEY: process.env.OPENGATEWAY_API_KEY, + OPENGATEWAY_BASE_URL: process.env.OPENGATEWAY_BASE_URL, + OPENCODE_API_KEY: process.env.OPENCODE_API_KEY, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED: process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED, + CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID: process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, + CLAUDE_STREAM_IDLE_TIMEOUT_MS: process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS, +} + +const originalFetch = globalThis.fetch + +function restoreEnv(key: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } +} + +type OpenAIShimClient = { + beta: { + messages: { + create: ( + params: Record, + options?: Record, + ) => Promise & { + withResponse: () => Promise<{ data: AsyncIterable> }> + } + } + } +} + +function makeSseResponse(lines: string[]): Response { + const encoder = new TextEncoder() + return new Response( + new ReadableStream({ + start(controller) { + for (const line of lines) { + controller.enqueue(encoder.encode(line)) + } + controller.close() + }, + }), + { + headers: { + 'Content-Type': 'text/event-stream', + }, + }, + ) +} + +function withResponseUrl(response: Response, url: string): Response { + Object.defineProperty(response, 'url', { + value: url, + configurable: true, + }) + return response +} + +type StallingResponse = { + response: Response + cancelReasons: unknown[] + close: () => void +} + +function makeStallingResponse( + firstChunk: string, + url = 'https://api.example.test/v1/chat/completions', + contentType = 'text/event-stream', +): StallingResponse { + const encoder = new TextEncoder() + const cancelReasons: unknown[] = [] + let streamController: ReadableStreamDefaultController | undefined + let closed = false + + const response = new Response( + new ReadableStream({ + start(controller) { + streamController = controller + controller.enqueue(encoder.encode(firstChunk)) + }, + cancel(reason) { + closed = true + cancelReasons.push(reason) + }, + }), + { + headers: { + 'Content-Type': contentType, + }, + }, + ) + + return { + response: withResponseUrl(response, url), + cancelReasons, + close: () => { + if (closed) return + closed = true + try { + streamController?.close() + } catch { + // The test may already have cancelled the stream. + } + }, + } +} + +type ShimStream = AsyncIterable> & { + controller: AbortController +} + +type StreamDrainOutcome = + | { status: 'completed'; events: Array> } + | { + status: 'rejected' + events: Array> + error: unknown + } + +async function waitForPromise( + promise: Promise, + timeoutMs: number, + message: string, +): Promise { + let timer: ReturnType | undefined + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(message)), timeoutMs) + }), + ]) + } finally { + if (timer !== undefined) { + clearTimeout(timer) + } + } +} + +async function expectAbortStopsStream({ + abort, + cancelReasons, + expectedEventsBeforeAbort, + label, + stream, +}: { + abort: () => void + cancelReasons: unknown[] + expectedEventsBeforeAbort: number + label: string + stream: ShimStream +}): Promise { + const events: Array> = [] + let resolveReady!: () => void + const ready = new Promise(resolve => { + resolveReady = resolve + }) + + const drain = (async (): Promise => { + try { + for await (const event of stream) { + events.push(event) + if (events.length >= expectedEventsBeforeAbort) { + resolveReady() + } + } + return { status: 'completed', events } + } catch (error) { + return { status: 'rejected', events, error } + } + })() + + await waitForPromise( + ready, + 500, + `${label} did not produce initial stream events`, + ) + // Let the for-await loop ask the stream reader for the next chunk, so the + // abort has to wake a real pending read rather than only flipping a flag. + await Promise.resolve() + await Promise.resolve() + + abort() + + const outcome = await waitForPromise( + drain, + 500, + `${label} did not stop promptly after abort`, + ) + expect(cancelReasons).toHaveLength(1) + expect(outcome.status).toBe('rejected') + if (outcome.status === 'rejected') { + expect((outcome.error as { name?: unknown }).name).toBe('AbortError') + } + return outcome +} + +async function expectPausedAbortCancelsStream({ + cancelReasons, + label, + stream, +}: { + cancelReasons: unknown[] + label: string + stream: ShimStream +}): Promise>> { + const iterator = stream[Symbol.asyncIterator]() + const first = await waitForPromise( + iterator.next(), + 500, + `${label} did not produce first stream event`, + ) + expect(first.done).toBe(false) + + stream.controller.abort() + await waitForPromise( + (async () => { + for (let i = 0; i < 10; i++) { + if (cancelReasons.length > 0) return + await Promise.resolve() + } + throw new Error(`${label} did not cancel source on controller abort`) + })(), + 500, + `${label} did not cancel source on controller abort`, + ) + + const returned = await waitForPromise( + Promise.resolve(iterator.return?.()), + 500, + `${label} did not return promptly after abort while paused`, + ) + expect(cancelReasons).toHaveLength(1) + return returned as IteratorResult> +} + +async function expectBufferedAbortRejectsNext({ + expectedText, + label, + stream, +}: { + expectedText?: string + label: string + stream: ShimStream +}): Promise { + const iterator = stream[Symbol.asyncIterator]() + + try { + let firstDelta: Record | undefined + for (let i = 0; i < 5; i++) { + const next = await waitForPromise( + iterator.next(), + 500, + `${label} did not produce expected pre-abort events`, + ) + expect(next.done).toBe(false) + if (next.value?.type === 'content_block_delta') { + firstDelta = next.value + break + } + } + + expect(firstDelta).toBeDefined() + if (expectedText !== undefined) { + expect((firstDelta as { delta?: { text?: string } }).delta?.text).toBe(expectedText) + } + + stream.controller.abort() + const afterAbort = await waitForPromise( + iterator.next().then( + value => ({ status: 'resolved' as const, value }), + error => ({ status: 'rejected' as const, error }), + ), + 500, + `${label} did not stop after abort`, + ) + + if (afterAbort.status !== 'rejected') { + throw new Error(`${label} yielded after abort: ${JSON.stringify(afterAbort.value)}`) + } + expect((afterAbort.error as { name?: unknown }).name).toBe('AbortError') + } finally { + await Promise.resolve(iterator.return?.()).catch(() => {}) + } +} + +function makeOpenAIStreamFrame( + delta: Record, + finishReason: string | null = null, +): string { + return `data: ${JSON.stringify({ + id: 'chatcmpl-abort-test', + object: 'chat.completion.chunk', + created: 1_780_000_000, + model: 'test-model', + choices: [{ index: 0, delta, finish_reason: finishReason }], + })}\n\n` +} + +function makeStreamChunks(chunks: unknown[]): string[] { + return [ + ...chunks.map(chunk => `data: ${JSON.stringify(chunk)}\n\n`), + 'data: [DONE]\n\n', + ] +} + +function importFreshOpenAIShim( + cacheKey: string, +): Promise { + return import(`../openaiShim.ts?${cacheKey}`) +} + +type StreamIdleTestApi = { + StreamIdleTimeoutError: new (timeoutMs: number) => Error + getStreamIdleTimeoutMs: () => number + readWithIdleTimeout: ( + reader: ReadableStreamDefaultReader, + timeoutMs: number, + options?: { signal?: AbortSignal; onTimeout?: () => void }, + ) => Promise['read']>>> +} + +async function getStreamIdleTestApi(cacheKey: string): Promise { + const mod = await importFreshOpenAIShim(cacheKey) + const testApi = mod.__test as unknown as Partial + expect(typeof testApi.StreamIdleTimeoutError).toBe('function') + expect(typeof testApi.getStreamIdleTimeoutMs).toBe('function') + expect(typeof testApi.readWithIdleTimeout).toBe('function') + return testApi as StreamIdleTestApi +} + +function makeChatCompletionResponse(model: string): Response { + return new Response( + JSON.stringify({ + id: 'chatcmpl-test', + model, + choices: [ + { + message: { + role: 'assistant', + content: 'ok', + }, + finish_reason: 'stop', + }, + ], + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) +} + +async function captureChatCompletionRequest( + model = 'mimo-v2.5-pro', +): Promise<{ authorization: string | null; url: string | null }> { + let authorization: string | null = null + let url: string | null = null + + globalThis.fetch = (async (input, init) => { + url = String(input) + const headers = init?.headers as Record | undefined + authorization = headers?.Authorization ?? headers?.authorization ?? null + + return makeChatCompletionResponse(model) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + return { authorization, url } +} + +function makeCodexSseResponse(responseData: Record): Response { + const data = JSON.stringify(responseData) + return makeSseResponse([`event: response.completed\ndata: ${data}\n\n`]) +} + +beforeEach(async () => { + await acquireSharedMutationLock('openaiShim.test.ts') + process.env.OPENAI_BASE_URL = 'http://example.test/v1' + delete process.env.OPENAI_API_BASE + process.env.OPENAI_API_KEY = 'test-key' + delete process.env.OPENAI_API_KEYS + delete process.env.OPENAI_MODEL + delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AUTH_HEADER + delete process.env.OPENAI_AUTH_SCHEME + delete process.env.OPENAI_AUTH_HEADER_VALUE + delete process.env.CLAUDE_CODE_USE_GITHUB + delete process.env.GITHUB_COPILOT_KEY + delete process.env.GITHUB_ENTERPRISE_URL + delete process.env.GITHUB_TOKEN + delete process.env.GH_TOKEN + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.CLAUDE_CODE_USE_GEMINI + delete process.env.GEMINI_API_KEY + delete process.env.GOOGLE_API_KEY + delete process.env.GEMINI_ACCESS_TOKEN + delete process.env.GEMINI_AUTH_MODE + delete process.env.GEMINI_BASE_URL + delete process.env.GEMINI_MODEL + delete process.env.GOOGLE_CLOUD_PROJECT + delete process.env.ANTHROPIC_CUSTOM_HEADERS + delete process.env.NVIDIA_API_KEY + delete process.env.NVIDIA_NIM + delete process.env.MINIMAX_API_KEY + delete process.env.BNKR_API_KEY + delete process.env.BANKR_BASE_URL + delete process.env.BANKR_MODEL + delete process.env.OPENROUTER_API_KEY + delete process.env.DEEPSEEK_API_KEY + delete process.env.MIMO_API_KEY + delete process.env.OPENGATEWAY_API_KEY + delete process.env.OPENGATEWAY_BASE_URL + delete process.env.OPENCODE_API_KEY + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED + delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID + delete process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS +}) + +afterEach(() => { + try { + restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL) + restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE) + restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY) + restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS) + restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL) + restoreEnv('OPENAI_API_FORMAT', originalEnv.OPENAI_API_FORMAT) + restoreEnv('OPENAI_AUTH_HEADER', originalEnv.OPENAI_AUTH_HEADER) + restoreEnv('OPENAI_AUTH_SCHEME', originalEnv.OPENAI_AUTH_SCHEME) + restoreEnv('OPENAI_AUTH_HEADER_VALUE', originalEnv.OPENAI_AUTH_HEADER_VALUE) + restoreEnv('CLAUDE_CODE_USE_GITHUB', originalEnv.CLAUDE_CODE_USE_GITHUB) + restoreEnv('GITHUB_COPILOT_KEY', originalEnv.GITHUB_COPILOT_KEY) + restoreEnv('GITHUB_ENTERPRISE_URL', originalEnv.GITHUB_ENTERPRISE_URL) + restoreEnv('GITHUB_TOKEN', originalEnv.GITHUB_TOKEN) + restoreEnv('GH_TOKEN', originalEnv.GH_TOKEN) + restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI) + restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI) + restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY) + restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY) + restoreEnv('GEMINI_ACCESS_TOKEN', originalEnv.GEMINI_ACCESS_TOKEN) + restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE) + restoreEnv('GEMINI_BASE_URL', originalEnv.GEMINI_BASE_URL) + restoreEnv('GEMINI_MODEL', originalEnv.GEMINI_MODEL) + restoreEnv('GOOGLE_CLOUD_PROJECT', originalEnv.GOOGLE_CLOUD_PROJECT) + restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS) + restoreEnv('NVIDIA_API_KEY', originalEnv.NVIDIA_API_KEY) + restoreEnv('NVIDIA_NIM', originalEnv.NVIDIA_NIM) + restoreEnv('MINIMAX_API_KEY', originalEnv.MINIMAX_API_KEY) + restoreEnv('BNKR_API_KEY', originalEnv.BNKR_API_KEY) + restoreEnv('BANKR_BASE_URL', originalEnv.BANKR_BASE_URL) + restoreEnv('BANKR_MODEL', originalEnv.BANKR_MODEL) + restoreEnv('OPENROUTER_API_KEY', originalEnv.OPENROUTER_API_KEY) + restoreEnv('DEEPSEEK_API_KEY', originalEnv.DEEPSEEK_API_KEY) + restoreEnv('MIMO_API_KEY', originalEnv.MIMO_API_KEY) + restoreEnv('OPENGATEWAY_API_KEY', originalEnv.OPENGATEWAY_API_KEY) + restoreEnv('OPENGATEWAY_BASE_URL', originalEnv.OPENGATEWAY_BASE_URL) + restoreEnv('OPENCODE_API_KEY', originalEnv.OPENCODE_API_KEY) + restoreEnv('CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED) + restoreEnv('CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID', originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID) + restoreEnv('CLAUDE_STREAM_IDLE_TIMEOUT_MS', originalEnv.CLAUDE_STREAM_IDLE_TIMEOUT_MS) + globalThis.fetch = originalFetch + _clearRegistryForTesting() + ensureIntegrationsLoaded() + } finally { + releaseSharedMutationLock() + } +}) + + +test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEYS pool', async () => { + process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' + process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' + process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-ogw-key') +}) + +test('gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback', async () => { + process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' + process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' + process.env.OPENAI_API_KEY = 'fake-generic-openai-key' + delete process.env.OPENGATEWAY_API_KEY + + const result = applyProviderFlag('gitlawb-opengateway', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-openai-pool-a') +}) + +test('OPENAI_API_KEYS rejects placeholder values before sending requests', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,SUA_CHAVE' + process.env.OPENAI_API_KEY = 'single-key-should-not-hide-invalid-pool' + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + return makeChatCompletionResponse('gpt-5.5') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow(/SUA_CHAVE|Authentication failed/) + + expect(authorizations).toEqual([]) +}) + +test('OPENAI_API_KEYS rotates to the next key on rate-limit failure', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + process.env.OPENAI_API_KEY = 'single-key-should-not-win' + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + + if (authorizations.length === 1) { + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + } + + return makeChatCompletionResponse('gpt-5.5') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + +test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + +test('comma-separated OPENAI_API_KEY rotates to the next key on rate-limit failure', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEY = 'key-a,key-b' + delete process.env.OPENAI_API_KEYS + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + + if (authorizations.length === 1) { + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + } + + return makeChatCompletionResponse('gpt-5.5') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + +test('OPENAI_API_KEYS does not rotate through pool on provider 5xx outage', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + + return new Response(JSON.stringify({ error: { message: 'server error' } }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(authorizations).toEqual(['Bearer key-a']) +}) + +test('OPENAI_API_KEYS preserves cooldown state across client requests', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + + if (authorizations.length === 1) { + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + } + + return makeChatCompletionResponse('gpt-5.5') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + for (let i = 0; i < 2; i++) { + await client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + } + + expect(authorizations).toEqual([ + 'Bearer key-a', + 'Bearer key-b', + 'Bearer key-b', + ]) +}) + +test('OPENAI_API_KEYS rotates Azure api-key auth on auth failure', async () => { + const apiKeys: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://example.openai.azure.com/openai/deployments/test/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'azure-key-a,azure-key-b' + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + apiKeys.push(headers?.['api-key'] ?? null) + + if (apiKeys.length === 1) { + return new Response(JSON.stringify({ error: { message: 'unauthorized' } }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + + return makeChatCompletionResponse('gpt-5.5') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(apiKeys).toEqual(['azure-key-a', 'azure-key-b']) +}) + +test('OPENAI_API_KEYS does not reuse auth-disabled credentials across client requests', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + + return new Response(JSON.stringify({ error: { message: 'unauthorized' } }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello again' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + +test('OPENAI_API_KEYS permanently evicts 403 auth failures', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-5.5' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + + return new Response(JSON.stringify({ error: { message: 'forbidden' } }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello again' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + +test('strips credentials and query params from URL in fetch network error message', async () => { + process.env.OPENAI_BASE_URL = + 'https://user:password@internal.example.test/v1?token=abc123' + process.env.OPENAI_API_KEY = 'test-key' + + globalThis.fetch = asMockFetch(mock(async () => { + throw new TypeError( + 'fetch failed https://user:password@internal.example.test/v1?token=abc123/chat/completions', + ) + })) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + let caught: unknown + try { + await client.beta.messages.create({ + model: 'test-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + } catch (error) { + caught = error + } + + const message = (caught as Error).message + expect(message).toContain('internal.example.test') + expect(message).toContain('fetch failed') + expect(message).not.toContain('password') + expect(message).not.toContain('user:') + expect(message).not.toContain('token=abc123') +}) + +test('classifies localhost transport failures with actionable category marker', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + + const transportError = Object.assign(new TypeError('fetch failed'), { + code: 'ECONNREFUSED', + }) + + globalThis.fetch = asMockFetch(mock(async () => { + throw transportError + })) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.toThrow('openai_category=connection_refused') + + await expect( + client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.toThrow('local server is running') +}) + +test('transport failures are not labeled with HTTP status 503', async () => { + // Issue #971: ENETDOWN (and other transport errors) are emitted before any + // HTTP response is received. Reporting them as "503" makes users believe the + // upstream server returned 503 Service Unavailable. + process.env.OPENAI_BASE_URL = 'https://intranet.example.test/v1' + + const transportError = Object.assign(new TypeError('fetch failed'), { + code: 'ENETDOWN', + }) + + globalThis.fetch = asMockFetch(mock(async () => { + throw transportError + })) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + let caught: unknown + try { + await client.beta.messages.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + } catch (error) { + caught = error + } + + expect(caught).toBeDefined() + const err = caught as { status?: number; message: string; constructor: { name: string } } + expect(err.constructor.name).toBe('APIConnectionError') + expect(err.status).toBeUndefined() + expect(err.message).not.toMatch(/^503\b/) + expect(err.message).toContain('OpenAI API transport error') + expect(err.message).toContain('code=ENETDOWN') + expect(err.message).toContain('openai_category=network_error') +}) + +test('propagates AbortError without wrapping it as transport failure', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + + const abortError = new DOMException('The operation was aborted.', 'AbortError') + globalThis.fetch = asMockFetch(mock(async () => { + throw abortError + })) + + const controller = new AbortController() + controller.abort() + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create( + { + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, + { signal: controller.signal }, + ), + ).rejects.toBe(abortError) +}) + +test('classifies chat-completions endpoint 404 failures with endpoint_not_found marker', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434' + + globalThis.fetch = asMockFetch(mock(async () => + new Response('Not Found', { + status: 404, + headers: { + 'Content-Type': 'text/plain', + }, + }))) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.toThrow('openai_category=endpoint_not_found') +}) + +test('self-heals localhost resolution failures by retrying local loopback base URL', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + + const requestUrls: string[] = [] + globalThis.fetch = (async (input, _init) => { + const url = typeof input === 'string' ? input : input.url + requestUrls.push(url) + + if (url.includes('localhost')) { + const error = Object.assign(new TypeError('fetch failed'), { + code: 'ENOTFOUND', + }) + throw error + } + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'qwen2.5-coder:7b', + choices: [ + { + message: { + role: 'assistant', + content: 'hello from loopback', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 4, + completion_tokens: 3, + total_tokens: 7, + }, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).resolves.toBeDefined() + + expect(requestUrls[0]).toBe('http://localhost:11434/api/chat') + expect(requestUrls).toContain('http://127.0.0.1:11434/api/chat') +}) + +test('self-heals tool-call incompatibility by retrying local Ollama requests without tools', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + + const requestBodies: Array> = [] + globalThis.fetch = (async (_input, init) => { + const requestBody = JSON.parse(String(init?.body)) as Record + requestBodies.push(requestBody) + + if (requestBodies.length === 1) { + return new Response('tool_calls are not supported', { + status: 400, + headers: { + 'Content-Type': 'text/plain', + }, + }) + } + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'qwen2.5-coder:7b', + choices: [ + { + message: { + role: 'assistant', + content: 'fallback without tools', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 8, + completion_tokens: 4, + total_tokens: 12, + }, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + tools: [ + { + name: 'Read', + description: 'Read a file', + input_schema: { + type: 'object', + properties: { + filePath: { type: 'string' }, + }, + required: ['filePath'], + }, + }, + ], + max_tokens: 64, + stream: false, + }), + ).resolves.toBeDefined() + + expect(requestBodies).toHaveLength(2) + expect(Array.isArray(requestBodies[0]?.tools)).toBe(true) + expect(requestBodies[0]?.tool_choice).toBeUndefined() + expect( + requestBodies[1]?.tools === undefined || + (Array.isArray(requestBodies[1]?.tools) && requestBodies[1]?.tools.length === 0), + ).toBe(true) + expect(requestBodies[1]?.tool_choice).toBeUndefined() + expect(requestBodies[1]?.tool_stream).toBeUndefined() +}) + +test('Shim self-heals a JSON `tool_stream` rejection by retrying without it (#1950)', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + const requestBodies: Array> = [] + let callCount = 0 + globalThis.fetch = (async (_input, init) => { + requestBodies.push(JSON.parse(String(init?.body))) + callCount += 1 + if (callCount === 1) { + return new Response( + '{"error":{"message":"tool_stream is unsupported"}}', + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ) + } + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + // Must not throw — the self-heal retry succeeds. + await client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ], + max_tokens: 64, + stream: true, + }) + + // First attempt sent tool_stream; the self-heal dropped it and retried. + expect(requestBodies).toHaveLength(2) + expect(requestBodies[0]?.tool_stream).toBe(true) + expect(requestBodies[1]?.tool_stream).toBeUndefined() + // Tools are preserved across the retry. + expect(Array.isArray(requestBodies[1]?.tools)).toBe(true) +}) + +test('Shim stops after one tool_stream self-heal retry when the retry also fails (#1950)', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' + + const requestBodies: Array> = [] + globalThis.fetch = (async (_input, init) => { + requestBodies.push(JSON.parse(String(init?.body))) + return new Response( + '{"error":{"message":"tool_stream is unsupported"}}', + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [{ + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }], + max_tokens: 64, + stream: true, + }), + ).rejects.toThrow() + + expect(requestBodies).toHaveLength(2) + expect(requestBodies[0]?.tool_stream).toBe(true) + expect(requestBodies[1]?.tool_stream).toBeUndefined() +}) + +test('Shim retries a tool_stream rejection with the same pooled credential (#1950)', async () => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + const authorizations: Array = [] + let callCount = 0 + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + callCount += 1 + if (callCount === 1) { + return new Response( + '{"error":{"message":"Validation: Unsupported parameter(s): `tool_stream`"}}', + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ) + } + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5.2', + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'glm-5.2', + messages: [{ role: 'user', content: 'run pwd' }], + tools: [{ + name: 'Bash', + description: 'Run a shell command', + input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] }, + }], + max_tokens: 64, + stream: true, + }) + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-a']) +}) + + +test('strips Anthropic-specific headers on GitHub Codex transport requests', async () => { + let capturedHeaders: Headers | undefined + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_API_KEY = 'github-test-key' + process.env.GITHUB_TOKEN = 'stored-secret' + delete process.env.GITHUB_COPILOT_KEY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_MODEL + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers) + + return new Response('', { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await client.beta.messages.create( + { + model: 'github:gpt-5-codex', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }, + { + headers: { + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'prompt-caching-2024-07-31', + 'x-anthropic-additional-protection': 'true', + 'x-safe-header': 'keep-me', + }, + }, + ) + + expect(capturedHeaders?.get('anthropic-version')).toBeNull() + expect(capturedHeaders?.get('anthropic-beta')).toBeNull() + expect(capturedHeaders?.get('x-anthropic-additional-protection')).toBeNull() + expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me') + expect(capturedHeaders?.get('authorization')).toBe('Bearer github-test-key') + expect(capturedHeaders?.get('editor-plugin-version')).toBe('copilot-chat/0.26.7') +}) + + +test('strips Anthropic-specific headers on GitHub Codex transport with providerOverride API key', async () => { + let capturedHeaders: Headers | undefined + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_API_KEY = 'env-should-not-win' + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_MODEL + + globalThis.fetch = (async (_input, init) => { + capturedHeaders = new Headers(init?.headers) + + return new Response('', { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({ + providerOverride: { + model: 'github:gpt-5-codex', + baseURL: 'https://api.githubcopilot.com', + apiKey: 'provider-override-key', + }, + }) as OpenAIShimClient + + await client.beta.messages.create( + { + model: 'ignored', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }, + { + headers: { + 'anthropic-version': '2023-06-01', + 'x-claude-remote-session-id': 'remote-123', + 'x-safe-header': 'keep-me', + }, + }, + ) + + expect(capturedHeaders?.get('anthropic-version')).toBeNull() + expect(capturedHeaders?.get('x-claude-remote-session-id')).toBeNull() + expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me') + expect(capturedHeaders?.get('authorization')).toBe('Bearer provider-override-key') + expect(capturedHeaders?.get('editor-plugin-version')).toBe('copilot-chat/0.26.7') +}) + +test('GitHub Copilot 401 chat_completions retries with refreshed token', async () => { + const realModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' + + let fetchCallCount = 0 + let firstAuth: string | undefined + let secondAuth: string | undefined + + globalThis.fetch = ((_input, init) => { + fetchCallCount++ + const headers = init?.headers as Record | undefined + const auth = headers?.Authorization + + if (fetchCallCount === 1) { + firstAuth = auth + return Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'token expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + ) + } + + if (fetchCallCount === 2) { + secondAuth = auth + return Promise.resolve(makeChatCompletionResponse('gpt-4')) + } + + throw new Error(`unexpected fetch call #${fetchCallCount}`) + }) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-retry') + + const client = createClient({}) as OpenAIShimClient + + const response = await client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(process.env.GITHUB_TOKEN).toBe('refreshed-token') + expect(process.env.OPENAI_API_KEY).toBe('refreshed-token') + expect(fetchCallCount).toBe(2) + expect(firstAuth).toBe('Bearer initial-token') + expect(secondAuth).toBe('Bearer refreshed-token') + expect(response).toBeDefined() + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realModule) + } +}) + +test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => { + const realGithubModule = realGithubModelsCredentials + const realCodexModule = realCodexShim + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + let codexCallCount = 0 + let firstAuth: string | undefined + let secondAuth: string | undefined + + mock.module('../codexShim.js', () => ({ + ...realCodexModule, + performCodexRequest: mock(async (opts: { credentials: { apiKey: string } }) => { + codexCallCount++ + const apiKey = opts.credentials?.apiKey + + if (codexCallCount === 1) { + firstAuth = apiKey + throw APIError.generate(401, undefined, 'token expired', new Headers()) + } + + if (codexCallCount === 2) { + secondAuth = apiKey + return makeCodexSseResponse({ + response: { + id: 'resp_test', + output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }], + model: 'gpt-5', + usage: { input_tokens: 10, output_tokens: 5 }, + }, + }) + } + + throw new Error(`unexpected codex call #${codexCallCount}`) + }), + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-retry-codex') + + const client = createClient({}) as OpenAIShimClient + + const response = await client.beta.messages.create({ + model: 'gpt-5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(process.env.GITHUB_TOKEN).toBe('refreshed-token') + expect(process.env.OPENAI_API_KEY).toBe('refreshed-token') + expect(codexCallCount).toBe(2) + expect(firstAuth).toBe('initial-token') + expect(secondAuth).toBe('refreshed-token') + expect(response).toBeDefined() + expect((response as Record).content).toBeDefined() + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + mock.module('../codexShim.js', () => realCodexModule) + } +}) + +test('GitHub Copilot 401 with credential pool uses refreshed token not pool key', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + delete process.env.OPENAI_API_KEY + process.env.OPENAI_API_KEYS = 'initial-token,second-key' + process.env.GITHUB_TOKEN = 'initial-token' + + let fetchCallCount = 0 + let usedAuthHeaders: string[] = [] + + globalThis.fetch = ((_input, init) => { + fetchCallCount++ + const headers = init?.headers as Record | undefined + usedAuthHeaders.push(headers?.Authorization ?? '') + + if (fetchCallCount === 1) { + return Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'token expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + ) + } + + return Promise.resolve(makeChatCompletionResponse('gpt-4')) + }) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-pool') + + const client = createClient({}) as OpenAIShimClient + + const response = await client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(fetchCallCount).toBe(2) + expect(usedAuthHeaders[0]).toBe('Bearer initial-token') + expect(usedAuthHeaders[1]).toBe('Bearer refreshed-token') + expect(response).toBeDefined() + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + +test('GitHub Copilot 401 with "token has expired" triggers refresh', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' + + let fetchCallCount = 0 + + globalThis.fetch = ((_input, init) => { + fetchCallCount++ + + if (fetchCallCount === 1) { + return Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'token has expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + ) + } + + return Promise.resolve(makeChatCompletionResponse('gpt-4')) + }) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-has-expired') + + const client = createClient({}) as OpenAIShimClient + + const response = await client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(fetchCallCount).toBe(2) + expect(response).toBeDefined() + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + +test('GitHub Copilot 401 without expired-token message does not trigger refresh', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => true) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' + + let fetchCallCount = 0 + + globalThis.fetch = ((_input) => { + fetchCallCount++ + return Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'invalid token' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + ) + }) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-no-refresh') + + const client = createClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(refreshSpy).toHaveBeenCalledTimes(0) + expect(fetchCallCount).toBe(1) + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + +test('GitHub Copilot 401 refresh returning same token does not update auth', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'initial-token' + process.env.OPENAI_API_KEY = 'initial-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' + + let fetchCallCount = 0 + let usedAuthHeaders: string[] = [] + + globalThis.fetch = ((_input, init) => { + fetchCallCount++ + const headers = init?.headers as Record | undefined + usedAuthHeaders.push(headers?.Authorization ?? '') + + return Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'token expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + ) + }) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-same-token') + + const client = createClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(fetchCallCount).toBeGreaterThanOrEqual(2) + expect(usedAuthHeaders.every(h => h === 'Bearer initial-token')).toBe(true) + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + +test('GitHub Copilot 401 codex_responses with providerOverride does not trigger refresh', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'stored-copilot-token' + process.env.GITHUB_TOKEN = 'stored-copilot-token' + + // Mock fetch so performCodexRequest gets a 401 response (no codexShim mock needed) + globalThis.fetch = (() => + Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'token expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + )) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-override-codex') + + // providerOverride.apiKey differs from OPENAI_API_KEY → credential source gate blocks refresh + const client = createClient({ + providerOverride: { model: 'gpt-5', baseURL: 'https://api.githubcopilot.com', apiKey: 'override-token' }, + }) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'gpt-5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(refreshSpy).toHaveBeenCalledTimes(0) + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + +test('GitHub Copilot 401 chat_completions with providerOverride does not trigger refresh', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'stored-copilot-token' + process.env.GITHUB_TOKEN = 'stored-copilot-token' + + globalThis.fetch = (() => + Promise.resolve( + new Response( + JSON.stringify({ error: { message: 'token expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ), + )) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-override-chat') + + // providerOverride.apiKey differs from OPENAI_API_KEY → credential source gate blocks refresh + const client = createClient({ + providerOverride: { model: 'gpt-4', baseURL: 'https://api.githubcopilot.com', apiKey: 'override-token' }, + }) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow() + + expect(refreshSpy).toHaveBeenCalledTimes(0) + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + +// --- JSON fallback regression tests (#1749) ------------------------------- +// Some OpenAI-compatible providers ignore `stream: true` and return a full +// `application/json` chat completion. The fallback inside +// openaiStreamToAnthropic must route that response through the same +// non-streaming converter so tool_calls, Anthropic stop reasons, array +// content, and stripping are all preserved (jatmn CHANGES_REQUESTED). + +test('formats retry guidance from server response headers', () => { + expect(formatRetryAfterHint(new Response(null, { headers: { 'retry-after': '12' } }))) + .toBe(' (Retry-After: 12)') + expect(formatRetryAfterHint(new Response())).toBe('') +}) + +test('waits for the requested retry delay', async () => { + let scheduledDelay: number | undefined + let runScheduledCallback: (() => void) | undefined + let settled = false + + const pending = sleepMs(5, (callback, delay) => { + scheduledDelay = delay + runScheduledCallback = callback + }) + void pending.then(() => { + settled = true + }) + + expect(scheduledDelay).toBe(5) + expect(settled).toBe(false) + runScheduledCallback?.() + await pending + expect(settled).toBe(true) +}) diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts new file mode 100644 index 0000000000..1baaafcf3e --- /dev/null +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -0,0 +1,1007 @@ +import type { CredentialLease, CredentialPool } from '../credentialPool.js' +import type { OpenAICompatibilityFailure } from '../openaiErrorClassification.js' +import type { OpenAIShimRuntimeContext } from '../../../integrations/runtimeMetadata.js' + +export function formatRetryAfterHint(response: Response): string { + const retryAfter = response.headers.get('retry-after') + return retryAfter ? ` (Retry-After: ${retryAfter})` : '' +} + +export function sleepMs( + ms: number, + schedule: (callback: () => void, delay: number) => unknown = setTimeout, +): Promise { + return new Promise((resolve) => schedule(resolve, ms)) +} + +type CompatibilityFailure = Pick< + OpenAICompatibilityFailure, + 'category' | 'hint' | 'requestUrl' +> + +type ClassifiedFailure = OpenAICompatibilityFailure + +type GeminiCredential = { + kind: string + credential?: string + projectId?: string +} + +type RequestExecutorContext = { + defaultHeaders: Record + providerOverride?: { apiKey?: string } + getCredentialPool: (rawCredentials: string) => CredentialPool | null + filterAnthropicHeaders: ( + headers?: Record, + ) => Record + isGeminiMode: () => boolean + resolveRouteCredentialValue: (input: { + routeId?: string + baseUrl: string + processEnv: NodeJS.ProcessEnv + }) => string | undefined + isXaiBaseUrl: (baseUrl: string) => boolean + parseCredentialList: (value?: string) => string[] + resolveXaiAccessToken: () => Promise + hasInvalidCredentialPlaceholder: (value: string) => boolean + buildOpenAICompatibilityErrorMessage: ( + message: string, + failure: CompatibilityFailure, + ) => string + isAzureStyleBaseUrl: ( + baseUrl: string, + processEnv: NodeJS.ProcessEnv, + ) => boolean + resolveGeminiCredential: ( + processEnv: NodeJS.ProcessEnv, + ) => Promise + COPILOT_HEADERS: Record + getSessionId: () => string + getLocalProviderRetryBaseUrls: (baseUrl: string) => string[] + buildOllamaChatUrl: (baseUrl: string) => string + logForDebugging: ( + message: string, + options?: { level: 'warn' | 'error' | 'info' | 'debug' }, + ) => void + redactUrlForDiagnostics: (url: string) => string + redactSecretValueForDisplay: ( + value: string, + source: NodeJS.ProcessEnv, + ) => string | undefined + headersWithRequestUrl: (headers: Headers, requestUrl: string) => Headers + classifyOpenAINetworkFailure: ( + error: unknown, + context: { url: string }, + ) => ClassifiedFailure + classifyOpenAIHttpFailure: (context: { + status: number + body: string + url?: string + hasImages: boolean + }) => ClassifiedFailure + fetchWithProxyRetry: (input: string, init: RequestInit) => Promise + formatRetryAfterHint: (response: Response) => string + redactUrlsInMessage: (message: string) => string + sleepMs: (ms: number) => Promise + shouldAttemptLocalToollessRetry: (input: { + baseUrl: string + hasTools: boolean + }) => boolean + refreshCopilotTokenOn401: () => Promise + isCopilotTokenExpiredError: (text: string) => boolean + convertOllamaStreamingResponse: ( + response: Response, + model: string, + ) => Response + convertOllamaNonStreamingResponse: ( + response: Response, + model: string, + ) => Promise + logApiCallStart: ( + provider: string, + model: string, + ) => { correlationId: string; startTime: number } + logApiCallEnd: ( + correlationId: string, + startTime: number, + model: string, + status: 'success' | 'error', + tokensIn: number, + tokensOut: number, + cached: boolean, + ) => void + stableStringifyJson: (value: unknown) => string + APIError: { + generate: ( + status: number | undefined, + error: object | undefined, + message: string, + headers: Headers, + ) => Error + } + GITHUB_429_MAX_RETRIES: number + GITHUB_429_BASE_DELAY_SEC: number + GITHUB_429_MAX_DELAY_SEC: number + request: { + baseUrl: string + resolvedModel: string + transport: string + } + params: { stream?: boolean; tools?: unknown[] } + options?: { headers?: Record; signal?: AbortSignal } + requestProcessEnv: NodeJS.ProcessEnv + fastPath: { skipStableStringify: boolean } + shimConfig: { + headers?: Record + endpointPath?: string + defaultAuthHeader?: { name: string; scheme?: string } + } + runtimeShimContext: OpenAIShimRuntimeContext + body: Record + effectiveTransport: string + useNativeOllamaChat: boolean + buildResponsesBody: () => Record + serializeBody: () => string + omitTools: { + responses: boolean + anthropic: boolean + gemini: boolean + } + isLocal: boolean + isGithub: boolean + isGithubCopilot: boolean + isGithubModels: boolean +} + +export async function executeOpenAIRequest( + context: RequestExecutorContext, +): Promise { + const { + defaultHeaders, + providerOverride, + getCredentialPool, + filterAnthropicHeaders, + isGeminiMode, + resolveRouteCredentialValue, + isXaiBaseUrl, + parseCredentialList, + resolveXaiAccessToken, + hasInvalidCredentialPlaceholder, + buildOpenAICompatibilityErrorMessage, + isAzureStyleBaseUrl, + resolveGeminiCredential, + COPILOT_HEADERS, + getSessionId, + getLocalProviderRetryBaseUrls, + buildOllamaChatUrl, + logForDebugging, + redactUrlForDiagnostics, + redactSecretValueForDisplay, + headersWithRequestUrl, + classifyOpenAINetworkFailure, + classifyOpenAIHttpFailure, + fetchWithProxyRetry, + formatRetryAfterHint, + redactUrlsInMessage, + sleepMs, + shouldAttemptLocalToollessRetry, + refreshCopilotTokenOn401, + isCopilotTokenExpiredError, + convertOllamaStreamingResponse, + convertOllamaNonStreamingResponse, + logApiCallStart, + logApiCallEnd, + stableStringifyJson, + APIError, + GITHUB_429_MAX_RETRIES, + GITHUB_429_BASE_DELAY_SEC, + GITHUB_429_MAX_DELAY_SEC, + request, + params, + options, + requestProcessEnv, + fastPath, + shimConfig, + runtimeShimContext, + body, + effectiveTransport, + useNativeOllamaChat, + buildResponsesBody, + serializeBody, + omitTools, + isLocal, + isGithub, + isGithubCopilot, + isGithubModels, + } = context + const baseHeaders: Record = { + 'Content-Type': 'application/json', + ...filterAnthropicHeaders(shimConfig.headers), + ...defaultHeaders, + ...filterAnthropicHeaders(options?.headers), + } + + const isGemini = isGeminiMode() + const routeCredential = resolveRouteCredentialValue({ + routeId: runtimeShimContext.routeId ?? undefined, + baseUrl: request.baseUrl, + processEnv: process.env, + }) + // xAI OAuth: when the active route is xAI and no API key is set, fall + // back to a stored OAuth access token (auto-refreshed). The token is + // sent as a Bearer to api.x.ai/v1 — same surface as an API key. + const isXaiRoute = + runtimeShimContext.routeId === 'xai' || isXaiBaseUrl(request.baseUrl) + const openAIApiKeysPoolRaw = + parseCredentialList(process.env.OPENAI_API_KEYS).length > 0 + ? process.env.OPENAI_API_KEYS + : undefined + const openAIApiKeyRaw = process.env.OPENAI_API_KEY?.trim() + const openAIApiKeyValues = parseCredentialList(openAIApiKeyRaw) + const openAIApiKey = openAIApiKeyValues[0] + const openAIApiKeyRawUsable = + openAIApiKeyValues.length > 0 ? openAIApiKeyRaw : undefined + const xaiOAuthToken = + isXaiRoute && + !providerOverride?.apiKey && + !routeCredential && + !openAIApiKeysPoolRaw && + !openAIApiKey + ? await resolveXaiAccessToken() + : undefined + const openAIApiKeyIsCopiedProviderKey = Boolean( + openAIApiKeyRawUsable && + [ + process.env.OPENGATEWAY_API_KEY, + process.env.NVIDIA_API_KEY, + process.env.BNKR_API_KEY, + process.env.XAI_API_KEY, + process.env.MIMO_API_KEY, + process.env.VENICE_API_KEY, + process.env.MINIMAX_API_KEY, + process.env.ATLAS_CLOUD_API_KEY, + process.env.NEARAI_API_KEY, + process.env.FIREWORKS_API_KEY, + ].some((value) => value?.trim() === openAIApiKeyRawUsable), + ) + const routeCredentialIsCopiedProviderKey = Boolean( + routeCredential && + openAIApiKeyRawUsable && + routeCredential === openAIApiKeyRawUsable && + openAIApiKeyIsCopiedProviderKey, + ) + const routeCredentialIsProviderSpecific = Boolean( + routeCredential && + (!openAIApiKeyRawUsable || + routeCredential !== openAIApiKeyRawUsable || + routeCredentialIsCopiedProviderKey), + ) + const routeCredentialIsGenericOpenAIFallback = Boolean( + !routeCredentialIsProviderSpecific && + routeCredential && + openAIApiKeyRawUsable && + routeCredential === openAIApiKeyRawUsable, + ) + const apiKeyRaw = + providerOverride?.apiKey ?? + (openAIApiKeyIsCopiedProviderKey ? openAIApiKeyRawUsable : undefined) ?? + (routeCredentialIsGenericOpenAIFallback ? undefined : routeCredential) ?? + openAIApiKeysPoolRaw ?? + routeCredential ?? + (openAIApiKeyRawUsable || xaiOAuthToken || '') + // A catalog-level auth header is part of the selected model's transport + // contract. Ignore global custom auth left behind by another route so it + // cannot replace that model-specific header or credential. + const catalogAuthHeader = + runtimeShimContext.catalogEntry?.transportOverrides?.openaiShim + ?.defaultAuthHeader + const configuredAuthHeaderValue = catalogAuthHeader + ? undefined + : process.env.OPENAI_AUTH_HEADER_VALUE?.trim() + if (configuredAuthHeaderValue && /[\r\n]/.test(configuredAuthHeaderValue)) { + throw new Error( + 'OPENAI_AUTH_HEADER_VALUE must not contain CR/LF characters', + ) + } + const customAuthHeader = catalogAuthHeader + ? undefined + : process.env.OPENAI_AUTH_HEADER?.trim() + const hasCustomAuthHeader = Boolean( + customAuthHeader && /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(customAuthHeader), + ) + const explicitCustomAuthHeaderValue = hasCustomAuthHeader + ? configuredAuthHeaderValue + : '' + if ( + !explicitCustomAuthHeaderValue && + hasInvalidCredentialPlaceholder(apiKeyRaw) + ) { + throw APIError.generate( + 401, + undefined, + buildOpenAICompatibilityErrorMessage( + 'OpenAI API error 401: invalid credential pool placeholder SUA_CHAVE detected', + { + category: 'auth_invalid', + requestUrl: request.baseUrl, + }, + ), + new Headers(), + ) + } + const isAzure = isAzureStyleBaseUrl(request.baseUrl, requestProcessEnv) + + let isBankr = false + try { + isBankr = + runtimeShimContext.routeId === 'bankr' || + request.baseUrl.toLowerCase().includes('bankr') + } catch { + /* malformed URL — not Bankr */ + } + + const credentialPool = explicitCustomAuthHeaderValue + ? null + : getCredentialPool(apiKeyRaw) + const singleAuthValue = + explicitCustomAuthHeaderValue || + parseCredentialList(apiKeyRaw)[0] || + apiKeyRaw + + const buildHeadersForAttempt = async ( + credentialLease: CredentialLease | null, + ): Promise> => { + const headers: Record = { ...baseHeaders } + const authValue = + explicitCustomAuthHeaderValue || + refreshedCopilotToken || + credentialLease?.value || + (credentialPool ? '' : singleAuthValue) + + if (authValue) { + if (hasCustomAuthHeader && customAuthHeader) { + const defaultCustomAuthScheme = + customAuthHeader.toLowerCase() === 'authorization' ? 'bearer' : 'raw' + const customAuthScheme = + process.env.OPENAI_AUTH_SCHEME === 'raw' || + process.env.OPENAI_AUTH_SCHEME === 'bearer' + ? process.env.OPENAI_AUTH_SCHEME + : defaultCustomAuthScheme + headers[customAuthHeader] = + customAuthScheme === 'bearer' ? `Bearer ${authValue}` : authValue + } else if (isAzure) { + // Azure uses api-key header instead of Bearer token + headers['api-key'] = authValue + } else if (isBankr) { + // Bankr uses X-API-Key header instead of Bearer token + headers['X-API-Key'] = authValue + } else if (shimConfig.defaultAuthHeader?.name) { + headers[shimConfig.defaultAuthHeader.name] = + shimConfig.defaultAuthHeader.scheme === 'bearer' + ? `Bearer ${authValue}` + : authValue + } else { + headers.Authorization = `Bearer ${authValue}` + } + } else if (isGemini) { + const geminiCredential = await resolveGeminiCredential(process.env) + if (geminiCredential.kind !== 'none' && geminiCredential.credential) { + headers.Authorization = `Bearer ${geminiCredential.credential}` + if ( + geminiCredential.kind !== 'api-key' && + 'projectId' in geminiCredential && + geminiCredential.projectId + ) { + headers['x-goog-user-project'] = geminiCredential.projectId + } + } + } + + if (isGithubCopilot) { + Object.assign(headers, COPILOT_HEADERS) + } else if (isGithubModels) { + headers['Accept'] = 'application/vnd.github+json' + headers['X-GitHub-Api-Version'] = '2022-11-28' + } + + // xAI / Grok prompt caching. Pinning the session id via x-grok-conv-id + // routes follow-up requests to the same backend so xAI can reuse the + // cached system prompt and conversation history. Mirrors the Hermes + // implementation (RELEASE_v0.8.0 PR #5604). + if (isXaiRoute) { + headers['x-grok-conv-id'] ??= getSessionId() + } + + return headers + } + + const buildChatCompletionsUrl = (baseUrl: string): string => { + // Azure Cognitive Services / Azure OpenAI require a deployment-specific + // path and an api-version query parameter. + if (isAzure) { + const normalizedBaseUrl = (baseUrl.split(/[?#]/, 1)[0] ?? baseUrl).replace( + /\/+$/, + '', + ) + const apiVersion = + process.env.AZURE_OPENAI_API_VERSION ?? '2024-12-01-preview' + const deployment = encodeURIComponent( + request.resolvedModel ?? process.env.OPENAI_MODEL ?? 'gpt-4o', + ) + + // If base URL already contains /deployments/, use it as-is with api-version. + if (/\/deployments\//i.test(normalizedBaseUrl)) { + return `${normalizedBaseUrl}/chat/completions?api-version=${apiVersion}` + } + + // Strip trailing /v1 or /openai/v1 if present, then build Azure path. + const normalizedBase = normalizedBaseUrl + .replace(/\/(openai\/)?v1\/?$/, '') + .replace(/\/+$/, '') + + return `${normalizedBase}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}` + } + + return `${baseUrl}/chat/completions` + } + + const buildResponsesUrl = (baseUrl: string): string => { + const trimmedBase = baseUrl.replace(/\/+$/, '') + if (!isAzure) return `${trimmedBase}/responses` + + let normalizedBase = (trimmedBase.split(/[?#]/, 1)[0] ?? trimmedBase).replace( + /\/+$/, + '', + ) + for (;;) { + const stripped = normalizedBase + .replace(/\/(openai\/)?v1$/i, '') + .replace(/\/openai\/deployments\/[^/]+$/i, '') + .replace(/\/+$/, '') + if (stripped === normalizedBase) break + normalizedBase = stripped + } + return `${normalizedBase}/openai/v1/responses` + } + + const localRetryBaseUrls = isLocal + ? getLocalProviderRetryBaseUrls(request.baseUrl) + : [] + + const buildRequestUrl = (baseUrl: string): string => { + if (shimConfig.endpointPath) { + return `${baseUrl}${shimConfig.endpointPath}` + } + if (useNativeOllamaChat) { + return buildOllamaChatUrl(baseUrl) + } + return request.transport === 'responses' || + request.transport === 'responses_compat' + ? buildResponsesUrl(baseUrl) + : buildChatCompletionsUrl(baseUrl) + } + + let activeBaseUrl = request.baseUrl + let requestUrl = buildRequestUrl(activeBaseUrl) + const attemptedLocalBaseUrls = new Set([activeBaseUrl]) + let didRetryWithoutTools = false + let didRetryWithoutToolStream = false + let retryCredentialLease: CredentialLease | null = null + let didRefreshCopilotToken = false + let refreshedCopilotToken: string | undefined + + const promoteNextLocalBaseUrl = ( + reason: 'endpoint_not_found' | 'localhost_resolution_failed', + ): boolean => { + for (const candidateBaseUrl of localRetryBaseUrls) { + if (attemptedLocalBaseUrls.has(candidateBaseUrl)) { + continue + } + + const previousUrl = requestUrl + attemptedLocalBaseUrls.add(candidateBaseUrl) + activeBaseUrl = candidateBaseUrl + requestUrl = buildRequestUrl(activeBaseUrl) + + logForDebugging( + `[OpenAIShim] self-heal retry reason=${reason} method=POST from=${redactUrlForDiagnostics(previousUrl)} to=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, + { level: 'warn' }, + ) + + return true + } + + return false + } + + const bodyContainsImages = (): boolean => { + if (request.transport === 'responses') { + const responsesBody = buildResponsesBody() + const input = responsesBody.input as + Array> | undefined + if (!Array.isArray(input)) return false + return input.some((item) => { + const content = item.content as + Array> | undefined + return ( + Array.isArray(content) && + content.some((part) => part.type === 'input_image') + ) + }) + } + const messages = body.messages as Array> | undefined + if (!Array.isArray(messages)) return false + return messages.some((msg) => { + const content = msg.content + if (!Array.isArray(content)) return false + return content.some( + (part: Record) => part.type === 'image_url', + ) + }) + } + + // WHY: byte-identity required for implicit prefix caching in + // OpenAI/Kimi/DeepSeek. stableStringify sorts object keys at every + // depth so spurious insertion-order differences across rebuilds of + // `body` (spread-merge, conditional assignments above) don't bust + // the provider's prefix hash. + // + // Local backends do not implement prefix caching, so the deep key-sort + // is pure CPU overhead per request (issue #1016). Drop to the native + // `JSON.stringify` fast path when the fast-path config opts out. + let serializedBody = serializeBody() + + const refreshSerializedBody = (): void => { + serializedBody = serializeBody() + } + + const buildFetchInit = (headers: Record) => ({ + method: 'POST' as const, + headers, + body: serializedBody, + signal: options?.signal, + }) + + const maxSelfHealAttempts = isLocal ? localRetryBaseUrls.length + 1 : 0 + const credentialPoolAttempts = credentialPool?.size ?? 1 + let maxAttempts = + Math.max(isGithub ? GITHUB_429_MAX_RETRIES : 1, credentialPoolAttempts) + + maxSelfHealAttempts + + const throwClassifiedTransportError = ( + error: unknown, + requestUrl: string, + preclassifiedFailure?: ReturnType, + ): never => { + if (options?.signal?.aborted) { + throw error + } + + const failure = + preclassifiedFailure ?? + classifyOpenAINetworkFailure(error, { + url: requestUrl, + }) + const redactedUrl = redactUrlForDiagnostics(requestUrl) + const safeMessage = + redactSecretValueForDisplay( + redactUrlsInMessage(failure.message), + process.env, + ) || 'Request failed' + + logForDebugging( + `[OpenAIShim] transport failure category=${failure.category} retryable=${failure.retryable} code=${failure.code ?? 'unknown'} method=POST url=${redactedUrl} model=${request.resolvedModel} message=${safeMessage}`, + { level: 'warn' }, + ) + + throw APIError.generate( + 0, + undefined, + buildOpenAICompatibilityErrorMessage( + `OpenAI API transport error: ${safeMessage}${failure.code ? ` (code=${failure.code})` : ''}`, + failure, + ), + new Headers(), + ) + } + + const throwClassifiedHttpError = ( + status: number, + errorBody: string, + parsedBody: object | undefined, + responseHeaders: Headers, + requestUrl: string, + rateHint = '', + preclassifiedFailure?: ReturnType, + ): never => { + const failure = + preclassifiedFailure ?? + classifyOpenAIHttpFailure({ + status, + body: errorBody, + url: requestUrl, + hasImages: bodyContainsImages(), + }) + const failureWithUrl = { + ...failure, + requestUrl: failure.requestUrl ?? requestUrl, + } + const redactedUrl = redactUrlForDiagnostics(requestUrl) + + logForDebugging( + `[OpenAIShim] request failed category=${failure.category} retryable=${failure.retryable} status=${status} method=POST url=${redactedUrl} model=${request.resolvedModel}`, + { level: 'warn' }, + ) + + throw APIError.generate( + status, + parsedBody, + buildOpenAICompatibilityErrorMessage( + `OpenAI API error ${status}: ${errorBody}${rateHint}`, + failureWithUrl, + ), + headersWithRequestUrl(responseHeaders, requestUrl), + ) + } + + let response: Response | undefined + const provider = request.baseUrl.includes('nvidia') + ? 'nvidia-nim' + : request.baseUrl.includes('minimax') + ? 'minimax' + : request.baseUrl.includes('xiaomimimo') || + request.baseUrl.includes('mimo-v2') + ? 'xiaomi-mimo' + : request.baseUrl.includes('localhost:11434') || + request.baseUrl.includes('localhost:11435') + ? 'ollama' + : request.baseUrl.includes('anthropic') + ? 'anthropic' + : 'openai' + const { correlationId, startTime } = logApiCallStart( + provider, + request.resolvedModel, + ) + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const credentialLease = + retryCredentialLease ?? credentialPool?.next() ?? null + retryCredentialLease = null + if (credentialPool && !credentialLease) { + throw APIError.generate( + 401, + undefined, + buildOpenAICompatibilityErrorMessage( + 'OpenAI API error 401: credential pool exhausted after authentication failures', + { + category: 'auth_invalid', + requestUrl, + }, + ), + new Headers(), + ) + } + const headers = await buildHeadersForAttempt(credentialLease) + try { + response = await fetchWithProxyRetry(requestUrl, buildFetchInit(headers)) + } catch (error) { + const isAbortError = + options?.signal?.aborted === true || + (typeof DOMException !== 'undefined' && + error instanceof DOMException && + error.name === 'AbortError') || + (typeof error === 'object' && + error !== null && + 'name' in error && + error.name === 'AbortError') + + if (isAbortError) { + throw error + } + + const failure = classifyOpenAINetworkFailure(error, { + url: requestUrl, + }) + + if ( + isLocal && + failure.category === 'localhost_resolution_failed' && + promoteNextLocalBaseUrl('localhost_resolution_failed') + ) { + continue + } + + throwClassifiedTransportError(error, requestUrl, failure) + } + + // After the try/catch, response is guaranteed to be defined — the catch + // block always throws (throwClassifiedTransportError returns never). + if (!response) continue + + if (response.ok) { + credentialPool?.reportSuccess(credentialLease) + if (useNativeOllamaChat) { + response = params.stream + ? convertOllamaStreamingResponse(response, request.resolvedModel) + : await convertOllamaNonStreamingResponse( + response, + request.resolvedModel, + ) + } + let tokensIn = 0 + let tokensOut = 0 + // Skip clone() for streaming responses - it blocks until full body is received, + // defeating the purpose of streaming. Usage data is already sent via + // stream_options: { include_usage: true } and can be extracted from the stream. + if (!params.stream) { + try { + const bodyText = await response.text() + // Preserve routing metadata that `new Response()` drops to "". + // create() reads `response.url` to route between /responses, + // /messages, and Gemini conversion paths; losing it makes + // descriptor routes (OpenCode /messages, Gemini /models/gemini-*) + // fall through to the generic OpenAI converter and return the + // wrong message shape. `url` is a read-only getter on the + // prototype, so shadow it with an own property. + const originalUrl = response.url + const originalType = response.type + // Recreate the response immediately after reading the body, before + // JSON.parse — if parsing fails, downstream code can still read the + // body from the fresh Response instead of hitting "Body already used". + response = new Response(bodyText, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }) + if (originalUrl) { + try { + Object.defineProperty(response, 'url', { + value: originalUrl, + configurable: true, + }) + } catch { + /* some runtimes lock the property; routing falls back to transport */ + } + } + if (originalType && originalType !== 'basic') { + try { + Object.defineProperty(response, 'type', { + value: originalType, + configurable: true, + }) + } catch { + /* non-fatal: type is not used for response routing */ + } + } + const data = JSON.parse(bodyText) + tokensIn = data.usage?.prompt_tokens ?? 0 + tokensOut = data.usage?.completion_tokens ?? 0 + } catch { + /* ignore — response is already recreated with the body intact */ + } + } + logApiCallEnd( + correlationId, + startTime, + request.resolvedModel, + 'success', + tokensIn, + tokensOut, + false, + ) + return response + } + + if (isGithub && response.status === 429 && attempt < maxAttempts - 1) { + await response.text().catch(() => {}) + const delaySec = Math.min( + GITHUB_429_BASE_DELAY_SEC * 2 ** attempt, + GITHUB_429_MAX_DELAY_SEC, + ) + await sleepMs(delaySec * 1000) + continue + } + // Read body exactly once here — Response body is a stream that can only + // be consumed a single time. + const errorBody = await response.text().catch(() => 'unknown error') + const rateHint = + isGithub && response.status === 429 ? formatRetryAfterHint(response) : '' + + // If GitHub Copilot returns error about /chat/completions, + // try the /responses endpoint (needed for GPT-5+ models) + if (isGithub && response.status === 400) { + if ( + errorBody.includes('/chat/completions') || + errorBody.includes('not accessible') + ) { + const responsesUrl = `${request.baseUrl}/responses` + const responsesBody = buildResponsesBody() + + let responsesResponse!: Response + try { + responsesResponse = await fetchWithProxyRetry(responsesUrl, { + method: 'POST', + headers, + body: stableStringifyJson(responsesBody), + signal: options?.signal, + }) + } catch (error) { + throwClassifiedTransportError(error, responsesUrl) + } + + if (responsesResponse.ok) { + return responsesResponse + } + const responsesErrorBody = await responsesResponse + .text() + .catch(() => 'unknown error') + const responsesFailure = classifyOpenAIHttpFailure({ + status: responsesResponse.status, + body: responsesErrorBody, + hasImages: bodyContainsImages(), + }) + let responsesErrorResponse: object | undefined + try { + responsesErrorResponse = JSON.parse(responsesErrorBody) + } catch { + /* raw text */ + } + throwClassifiedHttpError( + responsesResponse.status, + responsesErrorBody, + responsesErrorResponse, + responsesResponse.headers, + responsesUrl, + '', + responsesFailure, + ) + } + } + + const failure = classifyOpenAIHttpFailure({ + status: response.status, + body: errorBody, + hasImages: bodyContainsImages(), + }) + + // GitHub Copilot 401 with expired token: force-refresh and retry once. + // Only applies to the Copilot endpoint, not GitHub Models API or custom + // routes, and only when the failing credential is the stored Copilot + // token (not a provider override, route credential, or custom auth). + // The refreshed token is stored in refreshedCopilotToken so the next + // iteration's buildHeadersForAttempt picks it up instead of the stale + // singleAuthValue captured before the loop. + if (isGithubCopilot && response.status === 401 && !didRefreshCopilotToken) { + if (isCopilotTokenExpiredError(errorBody)) { + const oldToken = headers.Authorization?.replace(/^Bearer\s+/i, '') || '' + if (oldToken && oldToken === (process.env.OPENAI_API_KEY ?? '')) { + didRefreshCopilotToken = true + const refreshed = await refreshCopilotTokenOn401() + if (refreshed) { + const newApiKey = process.env.OPENAI_API_KEY?.trim() || '' + if (newApiKey && newApiKey !== oldToken) { + refreshedCopilotToken = newApiKey + } + if (attempt < maxAttempts - 1) { + continue + } + } + } + } + } + + const credentialFailureKind = + failure.category === 'auth_invalid' && !failure.retryable + ? 'auth' + : response.status === 402 || response.status === 429 + ? 'cooldown' + : null + if (credentialPool && credentialPool.size > 1 && credentialFailureKind) { + credentialPool.reportFailure( + credentialLease, + credentialFailureKind, + CREDENTIAL_POOL_COOLDOWN_MS, + ) + if (attempt < maxAttempts - 1) { + logForDebugging( + `[OpenAIShim] credential pool retry status=${response.status} method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, + { level: 'warn' }, + ) + continue + } + } + + if ( + isLocal && + failure.category === 'endpoint_not_found' && + promoteNextLocalBaseUrl('endpoint_not_found') + ) { + continue + } + + const hasToolsPayload = + effectiveTransport === 'responses' || + effectiveTransport === 'responses_compat' || + effectiveTransport === 'anthropic_messages' || + effectiveTransport === 'gemini' + ? Array.isArray(params.tools) && params.tools.length > 0 + : Array.isArray(body.tools) && body.tools.length > 0 + + if ( + !didRetryWithoutTools && + failure.category === 'tool_call_incompatible' && + shouldAttemptLocalToollessRetry({ + baseUrl: activeBaseUrl, + hasTools: hasToolsPayload, + }) + ) { + didRetryWithoutTools = true + delete body.tools + delete body.tool_choice + delete body.tool_stream + omitTools.responses = true + omitTools.anthropic = true + omitTools.gemini = true + refreshSerializedBody() + + logForDebugging( + `[OpenAIShim] self-heal retry reason=tool_call_incompatible mode=toolless method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, + { level: 'warn' }, + ) + continue + } + + // `tool_stream` self-heal (#1950): some OpenAI-compatible gateways (e.g. + // NVIDIA NIM) reject the Z.AI-proprietary `tool_stream` parameter with a + // 400. Drop only that parameter and retry with tools intact — streaming + // tool calls simply aren't streamed on such gateways. This guards against + // regressions where the parameter slips through the catalog/runtime + // gating that normally suppresses it. + if ( + !didRetryWithoutToolStream && + failure.category === 'tool_stream_unsupported' && + body.tool_stream === true + ) { + didRetryWithoutToolStream = true + // Reserve one additional request only after this specific recovery is + // needed. Increasing the shared initial budget changes unrelated + // GitHub and credential-pool retry behavior. + maxAttempts += 1 + delete body.tool_stream + refreshSerializedBody() + // This retry only changes request formatting. Reuse the credential that + // received the rejection so a pool with unequal model access cannot + // turn a recoverable 400 into an unrelated authorization failure. + retryCredentialLease = credentialLease + + logForDebugging( + `[OpenAIShim] self-heal retry reason=tool_stream_unsupported method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, + { level: 'warn' }, + ) + continue + } + + let errorResponse: object | undefined + try { + errorResponse = JSON.parse(errorBody) + } catch { + /* raw text */ + } + throwClassifiedHttpError( + response.status, + errorBody, + errorResponse, + response.headers as unknown as Headers, + requestUrl, + rateHint, + failure, + ) + } + + throw APIError.generate( + 500, + undefined, + 'OpenAI shim: request loop exited unexpectedly', + new Headers(), + ) +} +const CREDENTIAL_POOL_COOLDOWN_MS = 30_000 From b0d8ba1600bb16f0b9d71d6219da493134c635b9 Mon Sep 17 00:00:00 2001 From: jatmn Date: Sat, 18 Jul 2026 16:46:43 -0700 Subject: [PATCH 02/25] fix(openai-shim): rebase executor extraction --- src/services/api/openaiShim.test.ts | 17 ++++++++--------- .../api/openaiShim/architecture.test.ts | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index 12e6f9af89..9e4db768e1 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -8359,8 +8359,7 @@ test('preserves valid tool_result and drops orphan tool_result', async () => { const orphanMessage = toolMessages.find(m => m.tool_call_id === 'orphan_call_2') expect(orphanMessage).toBeUndefined() - - // Actually, the semantic message IS injected here because the user block with orphan + // Actually, the semantic message IS injected here because the user block with orphan // tool result is converted to: // 1. Tool result (valid_call_1) -> role 'tool' // 2. User content ("What happened?") -> role 'user' @@ -8472,15 +8471,15 @@ test('injects semantic assistant message when tool result is followed by user me await client.beta.messages.create({ model: 'mistral-large-latest', messages: [ - { - role: 'assistant', - content: [{ type: 'tool_use', id: 'call_1', name: 'search', input: {} }] + { + role: 'assistant', + content: [{ type: 'tool_use', id: 'call_1', name: 'search', input: {} }] }, - { - role: 'user', + { + role: 'user', content: [ { type: 'tool_result', tool_use_id: 'call_1', content: 'Result' } - ] + ] }, { role: 'user', content: 'Next user query' }, ], @@ -8492,7 +8491,7 @@ test('injects semantic assistant message when tool result is followed by user me // Roles should be: assistant (tool_calls) -> tool -> assistant (semantic) -> user const roles = messages.map(m => m.role) expect(roles).toEqual(['assistant', 'tool', 'assistant', 'user']) - + const semanticMsg = messages[2] expect(semanticMsg.role).toBe('assistant') expect(semanticMsg.content).toBe('[Tool results received]') diff --git a/src/services/api/openaiShim/architecture.test.ts b/src/services/api/openaiShim/architecture.test.ts index 6bc75dd955..7adb176867 100644 --- a/src/services/api/openaiShim/architecture.test.ts +++ b/src/services/api/openaiShim/architecture.test.ts @@ -5,7 +5,7 @@ import { describe, expect, test } from 'bun:test' const facadePath = fileURLToPath(new URL('../openaiShim.ts', import.meta.url)) const moduleDirectory = fileURLToPath(new URL('.', import.meta.url)) -// The shared extraction seam is 5,123 lines. Each independently mergeable PR +// The shared extraction seam is 5,140 lines. Each independently mergeable PR // removes at least this many net lines when its representative module exists. // Detecting modules makes the ceiling tighten regardless of merge order. const extractionDeltas = [ @@ -18,7 +18,7 @@ const extractionDeltas = [ ['streamConversion.ts', 1_072], ['clientDispatch.ts', 182], ['requestPlanner.ts', 304], - ['requestExecutor.ts', 716], + ['requestExecutor.ts', 704], ] as const describe('openaiShim façade architecture', () => { @@ -30,6 +30,6 @@ describe('openaiShim façade architecture', () => { ) const facadeLines = readFileSync(facadePath, 'utf8').trimEnd().split('\n').length - expect(facadeLines).toBeLessThanOrEqual(5_123 - activeReduction) + expect(facadeLines).toBeLessThanOrEqual(5_140 - activeReduction) }) }) From 7ef850b82355e62fbff40dd515effadca6b52b68 Mon Sep 17 00:00:00 2001 From: jatmn Date: Sat, 18 Jul 2026 16:52:11 -0700 Subject: [PATCH 03/25] test(openai-shim): preserve local stream options coverage --- .../requestExecutor.integration.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/services/api/openaiShim/requestExecutor.integration.test.ts b/src/services/api/openaiShim/requestExecutor.integration.test.ts index 1ebfe9d5dc..70ca62330e 100644 --- a/src/services/api/openaiShim/requestExecutor.integration.test.ts +++ b/src/services/api/openaiShim/requestExecutor.integration.test.ts @@ -3021,6 +3021,27 @@ test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_comple expect(requestBody?.store).toBeUndefined() }) +test('does not send stream_options to local OpenAI-compatible servers', async () => { + process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8000/v1' + + globalThis.fetch = (async (_input, init) => { + const body = JSON.parse(String(init?.body)) + expect(body.stream).toBe(true) + expect(body.stream_options).toBeUndefined() + return new Response('', { + headers: { 'Content-Type': 'text/event-stream' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'local-vllm-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) +}) + test('Mistral: strips unsupported store on chat_completions (#739)', async () => { process.env.OPENAI_BASE_URL = 'https://api.mistral.ai/v1' process.env.OPENAI_API_KEY = 'mistral-test' From e508f0b6610225d86f8bb032c9fa4aa4c3d48782 Mon Sep 17 00:00:00 2001 From: jatmn Date: Sat, 18 Jul 2026 16:54:30 -0700 Subject: [PATCH 04/25] test(openai-shim): isolate Azure compatibility state --- .../api/openaiShim/requestExecutor.integration.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/api/openaiShim/requestExecutor.integration.test.ts b/src/services/api/openaiShim/requestExecutor.integration.test.ts index 70ca62330e..cdb923f9e8 100644 --- a/src/services/api/openaiShim/requestExecutor.integration.test.ts +++ b/src/services/api/openaiShim/requestExecutor.integration.test.ts @@ -20,6 +20,7 @@ const originalEnv = { OPENAI_API_KEYS: process.env.OPENAI_API_KEYS, OPENAI_MODEL: process.env.OPENAI_MODEL, OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT, + OPENAI_AZURE_STYLE: process.env.OPENAI_AZURE_STYLE, OPENAI_AUTH_HEADER: process.env.OPENAI_AUTH_HEADER, OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE, @@ -435,6 +436,7 @@ beforeEach(async () => { delete process.env.OPENAI_API_KEYS delete process.env.OPENAI_MODEL delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AZURE_STYLE delete process.env.OPENAI_AUTH_HEADER delete process.env.OPENAI_AUTH_SCHEME delete process.env.OPENAI_AUTH_HEADER_VALUE @@ -477,6 +479,7 @@ afterEach(() => { restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS) restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL) restoreEnv('OPENAI_API_FORMAT', originalEnv.OPENAI_API_FORMAT) + restoreEnv('OPENAI_AZURE_STYLE', originalEnv.OPENAI_AZURE_STYLE) restoreEnv('OPENAI_AUTH_HEADER', originalEnv.OPENAI_AUTH_HEADER) restoreEnv('OPENAI_AUTH_SCHEME', originalEnv.OPENAI_AUTH_SCHEME) restoreEnv('OPENAI_AUTH_HEADER_VALUE', originalEnv.OPENAI_AUTH_HEADER_VALUE) From dcc2da9f7f55a9f1565544f050425a50375695bf Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:48:57 -0700 Subject: [PATCH 05/25] fix(openai-shim): preserve executor retry contracts --- .../api/openaiShim/architecture.test.ts | 4 +-- .../requestExecutor.integration.test.ts | 3 ++ .../api/openaiShim/requestExecutor.test.ts | 31 +++++++++++++++++++ .../api/openaiShim/requestExecutor.ts | 9 ++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/services/api/openaiShim/architecture.test.ts b/src/services/api/openaiShim/architecture.test.ts index 7adb176867..5410cac7b7 100644 --- a/src/services/api/openaiShim/architecture.test.ts +++ b/src/services/api/openaiShim/architecture.test.ts @@ -5,7 +5,7 @@ import { describe, expect, test } from 'bun:test' const facadePath = fileURLToPath(new URL('../openaiShim.ts', import.meta.url)) const moduleDirectory = fileURLToPath(new URL('.', import.meta.url)) -// The shared extraction seam is 5,140 lines. Each independently mergeable PR +// The rebased shared extraction seam is 5,632 lines. Each independently mergeable PR // removes at least this many net lines when its representative module exists. // Detecting modules makes the ceiling tighten regardless of merge order. const extractionDeltas = [ @@ -30,6 +30,6 @@ describe('openaiShim façade architecture', () => { ) const facadeLines = readFileSync(facadePath, 'utf8').trimEnd().split('\n').length - expect(facadeLines).toBeLessThanOrEqual(5_140 - activeReduction) + expect(facadeLines).toBeLessThanOrEqual(5_632 - activeReduction) }) }) diff --git a/src/services/api/openaiShim/requestExecutor.integration.test.ts b/src/services/api/openaiShim/requestExecutor.integration.test.ts index cdb923f9e8..73cb094b79 100644 --- a/src/services/api/openaiShim/requestExecutor.integration.test.ts +++ b/src/services/api/openaiShim/requestExecutor.integration.test.ts @@ -54,6 +54,7 @@ const originalEnv = { CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED: process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED, CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID: process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, CLAUDE_STREAM_IDLE_TIMEOUT_MS: process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS, + HICAP_API_KEY: process.env.HICAP_API_KEY, } const originalFetch = globalThis.fetch @@ -470,6 +471,7 @@ beforeEach(async () => { delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED delete process.env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID delete process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS + delete process.env.HICAP_API_KEY }) afterEach(() => { try { @@ -513,6 +515,7 @@ afterEach(() => { restoreEnv('CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED', originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED) restoreEnv('CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID', originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID) restoreEnv('CLAUDE_STREAM_IDLE_TIMEOUT_MS', originalEnv.CLAUDE_STREAM_IDLE_TIMEOUT_MS) + restoreEnv('HICAP_API_KEY', originalEnv.HICAP_API_KEY) globalThis.fetch = originalFetch _clearRegistryForTesting() ensureIntegrationsLoaded() diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index c9723a15af..d5e3777443 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -620,6 +620,37 @@ test('OPENAI_API_KEYS rotates to the next key on rate-limit failure', async () = expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +test('GitHub 429 cools the active pooled credential before retrying', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://models.github.ai/inference' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + if (authorizations.length === 1) { + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + } + return makeChatCompletionResponse('gpt-5.5') + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { const authorizations: Array = [] diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 1baaafcf3e..131ba588b8 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -797,6 +797,13 @@ export async function executeOpenAIRequest( GITHUB_429_BASE_DELAY_SEC * 2 ** attempt, GITHUB_429_MAX_DELAY_SEC, ) + if (credentialPool && credentialPool.size > 1) { + credentialPool.reportFailure( + credentialLease, + 'cooldown', + CREDENTIAL_POOL_COOLDOWN_MS, + ) + } await sleepMs(delaySec * 1000) continue } @@ -837,6 +844,7 @@ export async function executeOpenAIRequest( const responsesFailure = classifyOpenAIHttpFailure({ status: responsesResponse.status, body: responsesErrorBody, + url: responsesUrl, hasImages: bodyContainsImages(), }) let responsesErrorResponse: object | undefined @@ -860,6 +868,7 @@ export async function executeOpenAIRequest( const failure = classifyOpenAIHttpFailure({ status: response.status, body: errorBody, + url: requestUrl, hasImages: bodyContainsImages(), }) From 3259f25a3054634e7bcc3a54fdb1fe11bd4626a2 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:50:43 -0700 Subject: [PATCH 06/25] fix(openai-shim): retain route credential isolation --- src/services/api/openaiShim.ts | 4 ++++ src/services/api/openaiShim/requestExecutor.ts | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 67cc4bbdb4..64d05ac063 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -1467,6 +1467,10 @@ class OpenAIShimMessages { return executeOpenAIRequest({ defaultHeaders: this.defaultHeaders, providerOverride: this.providerOverride, + routeAcceptsGenericOpenAICredentials: + runtimeShimContext.routeId === null || + getRouteDescriptor(runtimeShimContext.routeId)?.setup + .dedicatedCredentialsOnly !== true, getCredentialPool: value => this.getCredentialPool(value), filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, fetchWithProxyRetry, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools, }) diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 131ba588b8..284f12bcbd 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -30,6 +30,7 @@ type GeminiCredential = { type RequestExecutorContext = { defaultHeaders: Record providerOverride?: { apiKey?: string } + routeAcceptsGenericOpenAICredentials: boolean getCredentialPool: (rawCredentials: string) => CredentialPool | null filterAnthropicHeaders: ( headers?: Record, @@ -159,6 +160,7 @@ export async function executeOpenAIRequest( const { defaultHeaders, providerOverride, + routeAcceptsGenericOpenAICredentials, getCredentialPool, filterAnthropicHeaders, isGeminiMode, @@ -233,6 +235,7 @@ export async function executeOpenAIRequest( const isXaiRoute = runtimeShimContext.routeId === 'xai' || isXaiBaseUrl(request.baseUrl) const openAIApiKeysPoolRaw = + routeAcceptsGenericOpenAICredentials && parseCredentialList(process.env.OPENAI_API_KEYS).length > 0 ? process.env.OPENAI_API_KEYS : undefined @@ -284,11 +287,16 @@ export async function executeOpenAIRequest( ) const apiKeyRaw = providerOverride?.apiKey ?? - (openAIApiKeyIsCopiedProviderKey ? openAIApiKeyRawUsable : undefined) ?? + (openAIApiKeyIsCopiedProviderKey && + routeAcceptsGenericOpenAICredentials + ? openAIApiKeyRawUsable + : undefined) ?? (routeCredentialIsGenericOpenAIFallback ? undefined : routeCredential) ?? openAIApiKeysPoolRaw ?? routeCredential ?? - (openAIApiKeyRawUsable || xaiOAuthToken || '') + (routeAcceptsGenericOpenAICredentials + ? openAIApiKeyRawUsable || xaiOAuthToken || '' + : '') // A catalog-level auth header is part of the selected model's transport // contract. Ignore global custom auth left behind by another route so it // cannot replace that model-specific header or credential. From 2642ebe9d671be8b1d017f0a1edbbb93edc8cfbb Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:53:45 -0700 Subject: [PATCH 07/25] fix(openai-shim): preserve executor transport behavior --- src/services/api/openaiShim.ts | 2 +- .../api/openaiShim/requestExecutor.ts | 71 +++++++++++-------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 64d05ac063..46e7828093 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -1472,7 +1472,7 @@ class OpenAIShimMessages { getRouteDescriptor(runtimeShimContext.routeId)?.setup .dedicatedCredentialsOnly !== true, getCredentialPool: value => this.getCredentialPool(value), - filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, fetchWithProxyRetry, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools, + filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, isLongcatBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, markOpenAIRequestNonReplayable, fetchWithProxyRetry, fetchRequest: (url, init) => fetchWithHeadersDeadline(url, init, { callerSignal: options?.signal, timeoutMs: apiTimeoutMs }), isResponseHeadersTimeout: error => error instanceof ResponseHeadersTimeoutError, requestBodyContainsImages, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools, }) } diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 284f12bcbd..21ec67e329 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -42,6 +42,7 @@ type RequestExecutorContext = { processEnv: NodeJS.ProcessEnv }) => string | undefined isXaiBaseUrl: (baseUrl: string) => boolean + isLongcatBaseUrl: (baseUrl: string) => boolean parseCredentialList: (value?: string) => string[] resolveXaiAccessToken: () => Promise hasInvalidCredentialPlaceholder: (value: string) => boolean @@ -80,7 +81,11 @@ type RequestExecutorContext = { url?: string hasImages: boolean }) => ClassifiedFailure + markOpenAIRequestNonReplayable: (error: Error) => Error fetchWithProxyRetry: (input: string, init: RequestInit) => Promise + fetchRequest: (input: string, init: RequestInit) => Promise + isResponseHeadersTimeout: (error: unknown) => boolean + requestBodyContainsImages: (payload: Record | undefined) => boolean formatRetryAfterHint: (response: Response) => string redactUrlsInMessage: (message: string) => string sleepMs: (ms: number) => Promise @@ -166,6 +171,7 @@ export async function executeOpenAIRequest( isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, + isLongcatBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, @@ -182,7 +188,11 @@ export async function executeOpenAIRequest( headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, + markOpenAIRequestNonReplayable, fetchWithProxyRetry, + fetchRequest, + isResponseHeadersTimeout, + requestBodyContainsImages, formatRetryAfterHint, redactUrlsInMessage, sleepMs, @@ -450,7 +460,17 @@ export async function executeOpenAIRequest( return `${normalizedBase}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}` } - return `${baseUrl}/chat/completions` + const normalizedBase = baseUrl.replace(/\/+$/, '') + if (runtimeShimContext.routeId === 'longcat' && isLongcatBaseUrl(normalizedBase)) { + const pathname = new URL(normalizedBase).pathname + if (/^\/openai\/?$/.test(pathname)) { + return `${normalizedBase}/v1/chat/completions` + } + if (/^\/openai(?:\/v1)?\/chat\/completions$/.test(pathname)) { + return normalizedBase + } + } + return `${normalizedBase}/chat/completions` } const buildResponsesUrl = (baseUrl: string): string => { @@ -522,30 +542,12 @@ export async function executeOpenAIRequest( return false } - const bodyContainsImages = (): boolean => { - if (request.transport === 'responses') { - const responsesBody = buildResponsesBody() - const input = responsesBody.input as - Array> | undefined - if (!Array.isArray(input)) return false - return input.some((item) => { - const content = item.content as - Array> | undefined - return ( - Array.isArray(content) && - content.some((part) => part.type === 'input_image') - ) - }) + const bodyContainsImages = (serializedBody = serializeBody()): boolean => { + try { + return requestBodyContainsImages(JSON.parse(serializedBody) as Record) + } catch { + return false } - const messages = body.messages as Array> | undefined - if (!Array.isArray(messages)) return false - return messages.some((msg) => { - const content = msg.content - if (!Array.isArray(content)) return false - return content.some( - (part: Record) => part.type === 'image_url', - ) - }) } // WHY: byte-identity required for implicit prefix caching in @@ -602,7 +604,7 @@ export async function executeOpenAIRequest( { level: 'warn' }, ) - throw APIError.generate( + const apiError = APIError.generate( 0, undefined, buildOpenAICompatibilityErrorMessage( @@ -611,6 +613,9 @@ export async function executeOpenAIRequest( ), new Headers(), ) + throw failure.retryable + ? apiError + : markOpenAIRequestNonReplayable(apiError) } const throwClassifiedHttpError = ( @@ -690,7 +695,7 @@ export async function executeOpenAIRequest( } const headers = await buildHeadersForAttempt(credentialLease) try { - response = await fetchWithProxyRetry(requestUrl, buildFetchInit(headers)) + response = await fetchRequest(requestUrl, buildFetchInit(headers)) } catch (error) { const isAbortError = options?.signal?.aborted === true || @@ -706,9 +711,12 @@ export async function executeOpenAIRequest( throw error } - const failure = classifyOpenAINetworkFailure(error, { + const failure = { + ...classifyOpenAINetworkFailure(error, { url: requestUrl, - }) + }), + ...(isResponseHeadersTimeout(error) ? { retryable: false } : {}), + } if ( isLocal && @@ -833,7 +841,7 @@ export async function executeOpenAIRequest( let responsesResponse!: Response try { - responsesResponse = await fetchWithProxyRetry(responsesUrl, { + responsesResponse = await fetchRequest(responsesUrl, { method: 'POST', headers, body: stableStringifyJson(responsesBody), @@ -853,7 +861,7 @@ export async function executeOpenAIRequest( status: responsesResponse.status, body: responsesErrorBody, url: responsesUrl, - hasImages: bodyContainsImages(), + hasImages: requestBodyContainsImages(responsesBody), }) let responsesErrorResponse: object | undefined try { @@ -929,7 +937,8 @@ export async function executeOpenAIRequest( if ( isLocal && - failure.category === 'endpoint_not_found' && + (failure.category === 'endpoint_not_found' || + (response.status === 404 && failure.category === 'vision_not_supported')) && promoteNextLocalBaseUrl('endpoint_not_found') ) { continue From 1dc1cbb6600399fcefbc56287fe1d924b5fd9c39 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:56:09 -0700 Subject: [PATCH 08/25] fix(openai-shim): avoid duplicate local retries --- src/services/api/openaiShim/requestExecutor.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 21ec67e329..4d01384ed1 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -512,6 +512,7 @@ export async function executeOpenAIRequest( let activeBaseUrl = request.baseUrl let requestUrl = buildRequestUrl(activeBaseUrl) const attemptedLocalBaseUrls = new Set([activeBaseUrl]) + const attemptedLocalRequestUrls = new Set([requestUrl]) let didRetryWithoutTools = false let didRetryWithoutToolStream = false let retryCredentialLease: CredentialLease | null = null @@ -526,10 +527,17 @@ export async function executeOpenAIRequest( continue } + const candidateRequestUrl = buildRequestUrl(candidateBaseUrl) + if (attemptedLocalRequestUrls.has(candidateRequestUrl)) { + attemptedLocalBaseUrls.add(candidateBaseUrl) + continue + } + const previousUrl = requestUrl attemptedLocalBaseUrls.add(candidateBaseUrl) activeBaseUrl = candidateBaseUrl - requestUrl = buildRequestUrl(activeBaseUrl) + requestUrl = candidateRequestUrl + attemptedLocalRequestUrls.add(requestUrl) logForDebugging( `[OpenAIShim] self-heal retry reason=${reason} method=POST from=${redactUrlForDiagnostics(previousUrl)} to=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, From 33b65f0546a7b2b78ccf509aba9b893e241e7b47 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:56:56 -0700 Subject: [PATCH 09/25] fix(openai-shim): preserve LongCat credential routing --- src/services/api/openaiShim/requestExecutor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 4d01384ed1..f681b182c3 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -275,6 +275,7 @@ export async function executeOpenAIRequest( process.env.ATLAS_CLOUD_API_KEY, process.env.NEARAI_API_KEY, process.env.FIREWORKS_API_KEY, + process.env.LONGCAT_API_KEY, ].some((value) => value?.trim() === openAIApiKeyRawUsable), ) const routeCredentialIsCopiedProviderKey = Boolean( From 05ebfa6989ee0bc992d97af6e730745173b0a8f7 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:58:43 -0700 Subject: [PATCH 10/25] fix(openai-shim): retain executor abort contracts --- src/services/api/openaiShim/architecture.test.ts | 4 ++-- src/services/api/openaiShim/requestExecutor.test.ts | 2 +- src/services/api/openaiShim/requestExecutor.ts | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/services/api/openaiShim/architecture.test.ts b/src/services/api/openaiShim/architecture.test.ts index 5410cac7b7..10199d0589 100644 --- a/src/services/api/openaiShim/architecture.test.ts +++ b/src/services/api/openaiShim/architecture.test.ts @@ -5,7 +5,7 @@ import { describe, expect, test } from 'bun:test' const facadePath = fileURLToPath(new URL('../openaiShim.ts', import.meta.url)) const moduleDirectory = fileURLToPath(new URL('.', import.meta.url)) -// The rebased shared extraction seam is 5,632 lines. Each independently mergeable PR +// The rebased shared extraction seam is 5,636 lines. Each independently mergeable PR // removes at least this many net lines when its representative module exists. // Detecting modules makes the ceiling tighten regardless of merge order. const extractionDeltas = [ @@ -30,6 +30,6 @@ describe('openaiShim façade architecture', () => { ) const facadeLines = readFileSync(facadePath, 'utf8').trimEnd().split('\n').length - expect(facadeLines).toBeLessThanOrEqual(5_632 - activeReduction) + expect(facadeLines).toBeLessThanOrEqual(5_636 - activeReduction) }) }) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index d5e3777443..d3c781e179 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -1028,7 +1028,7 @@ test('propagates AbortError without wrapping it as transport failure', async () }, { signal: controller.signal }, ), - ).rejects.toBe(abortError) + ).rejects.toBe(controller.signal.reason) }) test('classifies chat-completions endpoint 404 failures with endpoint_not_found marker', async () => { diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index f681b182c3..9b26f0fd23 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -857,7 +857,11 @@ export async function executeOpenAIRequest( signal: options?.signal, }) } catch (error) { - throwClassifiedTransportError(error, responsesUrl) + const failure = { + ...classifyOpenAINetworkFailure(error, { url: responsesUrl }), + ...(isResponseHeadersTimeout(error) ? { retryable: false } : {}), + } + throwClassifiedTransportError(error, responsesUrl, failure) } if (responsesResponse.ok) { From 99d6966404de05e72653c98512c3935cf9ea4154 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 07:59:04 -0700 Subject: [PATCH 11/25] fix(openai-shim): preserve fallback cancellation --- src/services/api/openaiShim/requestExecutor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 9b26f0fd23..8ab3210c6e 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -857,6 +857,9 @@ export async function executeOpenAIRequest( signal: options?.signal, }) } catch (error) { + if (options?.signal?.aborted) { + throw options.signal.reason ?? error + } const failure = { ...classifyOpenAINetworkFailure(error, { url: responsesUrl }), ...(isResponseHeadersTimeout(error) ? { retryable: false } : {}), From 805b8b68e9796f6ebe8e3daa01c6e40f3251b0f7 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 08:17:54 -0700 Subject: [PATCH 12/25] fix(openai-shim): preserve executor retry and transport contracts --- src/services/api/openaiShim.ts | 2 +- .../api/openaiShim/requestExecutor.ts | 85 ++++++++++++------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 46e7828093..d1c165b305 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -1472,7 +1472,7 @@ class OpenAIShimMessages { getRouteDescriptor(runtimeShimContext.routeId)?.setup .dedicatedCredentialsOnly !== true, getCredentialPool: value => this.getCredentialPool(value), - filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, isLongcatBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, markOpenAIRequestNonReplayable, fetchWithProxyRetry, fetchRequest: (url, init) => fetchWithHeadersDeadline(url, init, { callerSignal: options?.signal, timeoutMs: apiTimeoutMs }), isResponseHeadersTimeout: error => error instanceof ResponseHeadersTimeoutError, requestBodyContainsImages, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools, + filterAnthropicHeaders, isGeminiMode, resolveRouteCredentialValue, isXaiBaseUrl, isLongcatBaseUrl, parseCredentialList, resolveXaiAccessToken, hasInvalidCredentialPlaceholder, buildOpenAICompatibilityErrorMessage, isAzureStyleBaseUrl, resolveGeminiCredential, COPILOT_HEADERS, getSessionId, getLocalProviderRetryBaseUrls, buildOllamaChatUrl, logForDebugging, redactUrlForDiagnostics, redactSecretValueForDisplay, headersWithRequestUrl, classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, markOpenAIRequestNonReplayable, fetchRequest: (url, init) => fetchWithHeadersDeadline(url, init, { callerSignal: options?.signal, timeoutMs: apiTimeoutMs }), isResponseHeadersTimeout: error => error instanceof ResponseHeadersTimeoutError, requestBodyContainsImages, formatRetryAfterHint, redactUrlsInMessage, sleepMs, shouldAttemptLocalToollessRetry, refreshCopilotTokenOn401, isCopilotTokenExpiredError, convertOllamaStreamingResponse, convertOllamaNonStreamingResponse, logApiCallStart, logApiCallEnd, stableStringifyJson, APIError, GITHUB_429_MAX_RETRIES, GITHUB_429_BASE_DELAY_SEC, GITHUB_429_MAX_DELAY_SEC, request, params, options, requestProcessEnv, fastPath, shimConfig, runtimeShimContext, body, effectiveTransport, useNativeOllamaChat, buildResponsesBody, serializeBody, isLocal, isGithub, isGithubCopilot, isGithubModels, omitTools, }) } diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 8ab3210c6e..da3d9bde23 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -1,6 +1,10 @@ import type { CredentialLease, CredentialPool } from '../credentialPool.js' import type { OpenAICompatibilityFailure } from '../openaiErrorClassification.js' import type { OpenAIShimRuntimeContext } from '../../../integrations/runtimeMetadata.js' +import { + redactEncodedSecretSubstringsForDisplay, + redactSecretSubstringsForDisplay, +} from '../../../utils/providerSecrets.js' export function formatRetryAfterHint(response: Response): string { const retryAfter = response.headers.get('retry-after') @@ -82,7 +86,6 @@ type RequestExecutorContext = { hasImages: boolean }) => ClassifiedFailure markOpenAIRequestNonReplayable: (error: Error) => Error - fetchWithProxyRetry: (input: string, init: RequestInit) => Promise fetchRequest: (input: string, init: RequestInit) => Promise isResponseHeadersTimeout: (error: unknown) => boolean requestBodyContainsImages: (payload: Record | undefined) => boolean @@ -189,7 +192,6 @@ export async function executeOpenAIRequest( classifyOpenAINetworkFailure, classifyOpenAIHttpFailure, markOpenAIRequestNonReplayable, - fetchWithProxyRetry, fetchRequest, isResponseHeadersTimeout, requestBodyContainsImages, @@ -237,7 +239,7 @@ export async function executeOpenAIRequest( const routeCredential = resolveRouteCredentialValue({ routeId: runtimeShimContext.routeId ?? undefined, baseUrl: request.baseUrl, - processEnv: process.env, + processEnv: requestProcessEnv, }) // xAI OAuth: when the active route is xAI and no API key is set, fall // back to a stored OAuth access token (auto-refreshed). The token is @@ -246,10 +248,10 @@ export async function executeOpenAIRequest( runtimeShimContext.routeId === 'xai' || isXaiBaseUrl(request.baseUrl) const openAIApiKeysPoolRaw = routeAcceptsGenericOpenAICredentials && - parseCredentialList(process.env.OPENAI_API_KEYS).length > 0 - ? process.env.OPENAI_API_KEYS + parseCredentialList(requestProcessEnv.OPENAI_API_KEYS).length > 0 + ? requestProcessEnv.OPENAI_API_KEYS : undefined - const openAIApiKeyRaw = process.env.OPENAI_API_KEY?.trim() + const openAIApiKeyRaw = requestProcessEnv.OPENAI_API_KEY?.trim() const openAIApiKeyValues = parseCredentialList(openAIApiKeyRaw) const openAIApiKey = openAIApiKeyValues[0] const openAIApiKeyRawUsable = @@ -265,17 +267,17 @@ export async function executeOpenAIRequest( const openAIApiKeyIsCopiedProviderKey = Boolean( openAIApiKeyRawUsable && [ - process.env.OPENGATEWAY_API_KEY, - process.env.NVIDIA_API_KEY, - process.env.BNKR_API_KEY, - process.env.XAI_API_KEY, - process.env.MIMO_API_KEY, - process.env.VENICE_API_KEY, - process.env.MINIMAX_API_KEY, - process.env.ATLAS_CLOUD_API_KEY, - process.env.NEARAI_API_KEY, - process.env.FIREWORKS_API_KEY, - process.env.LONGCAT_API_KEY, + requestProcessEnv.OPENGATEWAY_API_KEY, + requestProcessEnv.NVIDIA_API_KEY, + requestProcessEnv.BNKR_API_KEY, + requestProcessEnv.XAI_API_KEY, + requestProcessEnv.MIMO_API_KEY, + requestProcessEnv.VENICE_API_KEY, + requestProcessEnv.MINIMAX_API_KEY, + requestProcessEnv.ATLAS_CLOUD_API_KEY, + requestProcessEnv.NEARAI_API_KEY, + requestProcessEnv.FIREWORKS_API_KEY, + requestProcessEnv.LONGCAT_API_KEY, ].some((value) => value?.trim() === openAIApiKeyRawUsable), ) const routeCredentialIsCopiedProviderKey = Boolean( @@ -316,7 +318,7 @@ export async function executeOpenAIRequest( ?.defaultAuthHeader const configuredAuthHeaderValue = catalogAuthHeader ? undefined - : process.env.OPENAI_AUTH_HEADER_VALUE?.trim() + : requestProcessEnv.OPENAI_AUTH_HEADER_VALUE?.trim() if (configuredAuthHeaderValue && /[\r\n]/.test(configuredAuthHeaderValue)) { throw new Error( 'OPENAI_AUTH_HEADER_VALUE must not contain CR/LF characters', @@ -324,7 +326,7 @@ export async function executeOpenAIRequest( } const customAuthHeader = catalogAuthHeader ? undefined - : process.env.OPENAI_AUTH_HEADER?.trim() + : requestProcessEnv.OPENAI_AUTH_HEADER?.trim() const hasCustomAuthHeader = Boolean( customAuthHeader && /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(customAuthHeader), ) @@ -382,9 +384,9 @@ export async function executeOpenAIRequest( const defaultCustomAuthScheme = customAuthHeader.toLowerCase() === 'authorization' ? 'bearer' : 'raw' const customAuthScheme = - process.env.OPENAI_AUTH_SCHEME === 'raw' || - process.env.OPENAI_AUTH_SCHEME === 'bearer' - ? process.env.OPENAI_AUTH_SCHEME + requestProcessEnv.OPENAI_AUTH_SCHEME === 'raw' || + requestProcessEnv.OPENAI_AUTH_SCHEME === 'bearer' + ? requestProcessEnv.OPENAI_AUTH_SCHEME : defaultCustomAuthScheme headers[customAuthHeader] = customAuthScheme === 'bearer' ? `Bearer ${authValue}` : authValue @@ -403,7 +405,7 @@ export async function executeOpenAIRequest( headers.Authorization = `Bearer ${authValue}` } } else if (isGemini) { - const geminiCredential = await resolveGeminiCredential(process.env) + const geminiCredential = await resolveGeminiCredential(requestProcessEnv) if (geminiCredential.kind !== 'none' && geminiCredential.credential) { headers.Authorization = `Bearer ${geminiCredential.credential}` if ( @@ -443,9 +445,9 @@ export async function executeOpenAIRequest( '', ) const apiVersion = - process.env.AZURE_OPENAI_API_VERSION ?? '2024-12-01-preview' + requestProcessEnv.AZURE_OPENAI_API_VERSION ?? '2024-12-01-preview' const deployment = encodeURIComponent( - request.resolvedModel ?? process.env.OPENAI_MODEL ?? 'gpt-4o', + request.resolvedModel ?? requestProcessEnv.OPENAI_MODEL ?? 'gpt-4o', ) // If base URL already contains /deployments/, use it as-is with api-version. @@ -602,11 +604,19 @@ export async function executeOpenAIRequest( url: requestUrl, }) const redactedUrl = redactUrlForDiagnostics(requestUrl) - const safeMessage = - redactSecretValueForDisplay( + const encodedSecretRedactedMessage = + redactEncodedSecretSubstringsForDisplay( redactUrlsInMessage(failure.message), - process.env, - ) || 'Request failed' + requestProcessEnv, + ) ?? 'Request failed' + const substringRedactedMessage = + redactSecretSubstringsForDisplay( + encodedSecretRedactedMessage, + requestProcessEnv, + ) ?? 'Request failed' + const safeMessage = + redactSecretValueForDisplay(substringRedactedMessage, requestProcessEnv) || + 'Request failed' logForDebugging( `[OpenAIShim] transport failure category=${failure.category} retryable=${failure.retryable} code=${failure.code ?? 'unknown'} method=POST url=${redactedUrl} model=${request.resolvedModel} message=${safeMessage}`, @@ -860,6 +870,17 @@ export async function executeOpenAIRequest( if (options?.signal?.aborted) { throw options.signal.reason ?? error } + if ( + (typeof DOMException !== 'undefined' && + error instanceof DOMException && + error.name === 'AbortError') || + (typeof error === 'object' && + error !== null && + 'name' in error && + error.name === 'AbortError') + ) { + throw error + } const failure = { ...classifyOpenAINetworkFailure(error, { url: responsesUrl }), ...(isResponseHeadersTimeout(error) ? { retryable: false } : {}), @@ -914,15 +935,16 @@ export async function executeOpenAIRequest( if (isGithubCopilot && response.status === 401 && !didRefreshCopilotToken) { if (isCopilotTokenExpiredError(errorBody)) { const oldToken = headers.Authorization?.replace(/^Bearer\s+/i, '') || '' - if (oldToken && oldToken === (process.env.OPENAI_API_KEY ?? '')) { + if (oldToken && oldToken === (requestProcessEnv.OPENAI_API_KEY ?? '')) { didRefreshCopilotToken = true const refreshed = await refreshCopilotTokenOn401() if (refreshed) { - const newApiKey = process.env.OPENAI_API_KEY?.trim() || '' + const newApiKey = requestProcessEnv.OPENAI_API_KEY?.trim() || '' if (newApiKey && newApiKey !== oldToken) { refreshedCopilotToken = newApiKey } if (attempt < maxAttempts - 1) { + retryCredentialLease = credentialLease continue } } @@ -984,6 +1006,7 @@ export async function executeOpenAIRequest( omitTools.anthropic = true omitTools.gemini = true refreshSerializedBody() + retryCredentialLease = credentialLease logForDebugging( `[OpenAIShim] self-heal retry reason=tool_call_incompatible mode=toolless method=POST url=${redactUrlForDiagnostics(requestUrl)} model=${request.resolvedModel}`, From 87267672393ccd4f0ecf4297dec1c5ad34a5a948 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 08:58:05 -0700 Subject: [PATCH 13/25] fix(openai-shim): stabilize extracted executor smoke coverage --- src/services/api/openaiShim.test.ts | 53 +++++----- .../requestExecutor.integration.test.ts | 16 +++ .../api/openaiShim/requestExecutor.test.ts | 99 ++++--------------- .../api/openaiShim/requestExecutor.ts | 55 +++++++---- 4 files changed, 98 insertions(+), 125 deletions(-) diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index 9e4db768e1..e532921809 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -20,7 +20,6 @@ import { parseTextToolCalls, parseXmlToolCalls, } from './openaiShim.ts' -import * as realCodexShim from './codexShim.js' import * as realGithubModelsCredentials from '../../utils/githubModelsCredentials.js' type FetchType = typeof globalThis.fetch @@ -536,6 +535,7 @@ beforeEach(async () => { afterEach(() => { try { + mock.restore() restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL) restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE) restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY) @@ -11084,7 +11084,6 @@ test('GitHub Copilot 401 chat_completions retries with refreshed token', async ( // openaiShim test extraction seam 187 start: GitHub Copilot 401 codex_responses retries with refreshed token test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => { const realGithubModule = realGithubModelsCredentials - const realCodexModule = realCodexShim try { const refreshSpy = mock(async () => { process.env.GITHUB_TOKEN = 'refreshed-token' @@ -11101,32 +11100,33 @@ test('GitHub Copilot 401 codex_responses retries with refreshed token', async () let firstAuth: string | undefined let secondAuth: string | undefined - mock.module('./codexShim.js', () => ({ - ...realCodexModule, - performCodexRequest: mock(async (opts: { credentials: { apiKey: string } }) => { - codexCallCount++ - const apiKey = opts.credentials?.apiKey - - if (codexCallCount === 1) { - firstAuth = apiKey - throw APIError.generate(401, undefined, 'token expired', new Headers()) - } + globalThis.fetch = ((_, init) => { + codexCallCount++ + const headers = new Headers(init?.headers) + const apiKey = headers.get('authorization')?.replace(/^Bearer /, '') + + if (codexCallCount === 1) { + firstAuth = apiKey + return Promise.resolve(new Response(JSON.stringify({ error: { message: 'token expired' } }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + })) + } - if (codexCallCount === 2) { - secondAuth = apiKey - return makeCodexSseResponse({ - response: { - id: 'resp_test', - output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }], - model: 'gpt-5', - usage: { input_tokens: 10, output_tokens: 5 }, - }, - }) - } + if (codexCallCount === 2) { + secondAuth = apiKey + return Promise.resolve(makeCodexSseResponse({ + response: { + id: 'resp_test', + output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }], + model: 'gpt-5', + usage: { input_tokens: 10, output_tokens: 5 }, + }, + })) + } - throw new Error(`unexpected codex call #${codexCallCount}`) - }), - })) + throw new Error(`unexpected codex call #${codexCallCount}`) + }) as unknown as FetchType process.env.CLAUDE_CODE_USE_GITHUB = '1' process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' @@ -11155,7 +11155,6 @@ test('GitHub Copilot 401 codex_responses retries with refreshed token', async () expect((response as Record).content).toBeDefined() } finally { mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) - mock.module('./codexShim.js', () => realCodexModule) } }) // openaiShim test extraction seam 187 end diff --git a/src/services/api/openaiShim/requestExecutor.integration.test.ts b/src/services/api/openaiShim/requestExecutor.integration.test.ts index 73cb094b79..e123e05ce3 100644 --- a/src/services/api/openaiShim/requestExecutor.integration.test.ts +++ b/src/services/api/openaiShim/requestExecutor.integration.test.ts @@ -834,6 +834,22 @@ test('uses custom OpenAI-compatible auth header value when configured', async () expect(capturedHeaders?.get('authorization')).toBeNull() }) +test('rejects CR/LF in custom OpenAI-compatible auth header values', async () => { + process.env.OPENAI_AUTH_HEADER = 'api-key' + process.env.OPENAI_AUTH_HEADER_VALUE = 'valid-value\r\ninjected-header: value' + + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.toThrow('OPENAI_AUTH_HEADER_VALUE must not contain CR/LF characters') +}) + test('uses Hicap api-key auth header for the Hicap route', async () => { process.env.OPENAI_API_KEY = 'hicap-live-key' process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index d3c781e179..ee9c576385 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -11,7 +11,6 @@ import { } from '../errors.ts' import { createOpenAIShimClient, hasMistralApiHost } from '../openaiShim.ts' import { formatRetryAfterHint, sleepMs } from './requestExecutor.js' -import * as realCodexShim from '../codexShim.js' import * as realGithubModelsCredentials from '../../../utils/githubModelsCredentials.js' type FetchType = typeof globalThis.fetch @@ -480,6 +479,7 @@ beforeEach(async () => { afterEach(() => { try { + mock.restore() restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL) restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE) restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY) @@ -935,6 +935,26 @@ test('strips credentials and query params from URL in fetch network error messag expect(message).not.toContain('token=abc123') }) +test('redacts configured secrets from HTTP error messages', async () => { + process.env.OPENAI_API_KEY = 'super-secret-key' + + globalThis.fetch = asMockFetch(mock(async () => new Response( + JSON.stringify({ error: { message: 'upstream rejected super-secret-key' } }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ))) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + await expect( + client.beta.messages.create({ + model: 'test-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.not.toThrow('super-secret-key') +}) + test('classifies localhost transport failures with actionable category marker', async () => { process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' @@ -1509,83 +1529,6 @@ test('GitHub Copilot 401 chat_completions retries with refreshed token', async ( } }) -test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => { - const realGithubModule = realGithubModelsCredentials - const realCodexModule = realCodexShim - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'refreshed-token' - process.env.OPENAI_API_KEY = 'refreshed-token' - return true - }) - - mock.module('../../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) - - let codexCallCount = 0 - let firstAuth: string | undefined - let secondAuth: string | undefined - - mock.module('../codexShim.js', () => ({ - ...realCodexModule, - performCodexRequest: mock(async (opts: { credentials: { apiKey: string } }) => { - codexCallCount++ - const apiKey = opts.credentials?.apiKey - - if (codexCallCount === 1) { - firstAuth = apiKey - throw APIError.generate(401, undefined, 'token expired', new Headers()) - } - - if (codexCallCount === 2) { - secondAuth = apiKey - return makeCodexSseResponse({ - response: { - id: 'resp_test', - output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }], - model: 'gpt-5', - usage: { input_tokens: 10, output_tokens: 5 }, - }, - }) - } - - throw new Error(`unexpected codex call #${codexCallCount}`) - }), - })) - - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'initial-token' - process.env.GITHUB_TOKEN = 'initial-token' - - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-retry-codex') - - const client = createClient({}) as OpenAIShimClient - - const response = await client.beta.messages.create({ - model: 'gpt-5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(refreshSpy).toHaveBeenCalledTimes(1) - expect(process.env.GITHUB_TOKEN).toBe('refreshed-token') - expect(process.env.OPENAI_API_KEY).toBe('refreshed-token') - expect(codexCallCount).toBe(2) - expect(firstAuth).toBe('initial-token') - expect(secondAuth).toBe('refreshed-token') - expect(response).toBeDefined() - expect((response as Record).content).toBeDefined() - } finally { - mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) - mock.module('../codexShim.js', () => realCodexModule) - } -}) - test('GitHub Copilot 401 with credential pool uses refreshed token not pool key', async () => { const realGithubModule = realGithubModelsCredentials try { diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index da3d9bde23..59a95a4b15 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -25,6 +25,18 @@ type CompatibilityFailure = Pick< type ClassifiedFailure = OpenAICompatibilityFailure +function isAbortLikeError(error: unknown): boolean { + return ( + (typeof DOMException !== 'undefined' && + error instanceof DOMException && + error.name === 'AbortError') || + (typeof error === 'object' && + error !== null && + 'name' in error && + error.name === 'AbortError') + ) +} + type GeminiCredential = { kind: string credential?: string @@ -659,6 +671,25 @@ export async function executeOpenAIRequest( requestUrl: failure.requestUrl ?? requestUrl, } const redactedUrl = redactUrlForDiagnostics(requestUrl) + const encodedSecretRedactedBody = + redactEncodedSecretSubstringsForDisplay(errorBody, requestProcessEnv) ?? + 'unknown error' + const substringRedactedBody = + redactSecretSubstringsForDisplay( + encodedSecretRedactedBody, + requestProcessEnv, + ) ?? 'unknown error' + const safeErrorBody = + redactSecretValueForDisplay(substringRedactedBody, requestProcessEnv) || + 'unknown error' + let safeParsedBody = parsedBody + if (safeErrorBody !== errorBody) { + try { + safeParsedBody = JSON.parse(safeErrorBody) as object + } catch { + safeParsedBody = undefined + } + } logForDebugging( `[OpenAIShim] request failed category=${failure.category} retryable=${failure.retryable} status=${status} method=POST url=${redactedUrl} model=${request.resolvedModel}`, @@ -667,9 +698,9 @@ export async function executeOpenAIRequest( throw APIError.generate( status, - parsedBody, + safeParsedBody, buildOpenAICompatibilityErrorMessage( - `OpenAI API error ${status}: ${errorBody}${rateHint}`, + `OpenAI API error ${status}: ${safeErrorBody}${rateHint}`, failureWithUrl, ), headersWithRequestUrl(responseHeaders, requestUrl), @@ -716,15 +747,7 @@ export async function executeOpenAIRequest( try { response = await fetchRequest(requestUrl, buildFetchInit(headers)) } catch (error) { - const isAbortError = - options?.signal?.aborted === true || - (typeof DOMException !== 'undefined' && - error instanceof DOMException && - error.name === 'AbortError') || - (typeof error === 'object' && - error !== null && - 'name' in error && - error.name === 'AbortError') + const isAbortError = options?.signal?.aborted === true || isAbortLikeError(error) if (isAbortError) { throw error @@ -870,15 +893,7 @@ export async function executeOpenAIRequest( if (options?.signal?.aborted) { throw options.signal.reason ?? error } - if ( - (typeof DOMException !== 'undefined' && - error instanceof DOMException && - error.name === 'AbortError') || - (typeof error === 'object' && - error !== null && - 'name' in error && - error.name === 'AbortError') - ) { + if (isAbortLikeError(error)) { throw error } const failure = { From 0fa19e7f166830475c5bb348e56a06bc6a2fdcde Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 09:00:32 -0700 Subject: [PATCH 14/25] test(openai-shim): cover extracted executor retry contracts --- .../requestExecutor.integration.test.ts | 8 +-- .../api/openaiShim/requestExecutor.test.ts | 50 ++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/services/api/openaiShim/requestExecutor.integration.test.ts b/src/services/api/openaiShim/requestExecutor.integration.test.ts index e123e05ce3..c0c54b8b30 100644 --- a/src/services/api/openaiShim/requestExecutor.integration.test.ts +++ b/src/services/api/openaiShim/requestExecutor.integration.test.ts @@ -3046,10 +3046,9 @@ test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_comple test('does not send stream_options to local OpenAI-compatible servers', async () => { process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8000/v1' + let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { - const body = JSON.parse(String(init?.body)) - expect(body.stream).toBe(true) - expect(body.stream_options).toBeUndefined() + requestBody = JSON.parse(String(init?.body)) as Record return new Response('', { headers: { 'Content-Type': 'text/event-stream' }, }) @@ -3062,6 +3061,9 @@ test('does not send stream_options to local OpenAI-compatible servers', async () max_tokens: 64, stream: true, }) + + expect(requestBody?.stream).toBe(true) + expect(requestBody).not.toHaveProperty('stream_options') }) test('Mistral: strips unsupported store on chat_completions (#739)', async () => { diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index ee9c576385..b6cf8b04ac 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -1212,6 +1212,49 @@ test('self-heals tool-call incompatibility by retrying local Ollama requests wit expect(requestBodies[1]?.tool_stream).toBeUndefined() }) +test('reuses the pooled credential for a local toolless retry', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + const authorizations: Array = [] + let callCount = 0 + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + callCount += 1 + if (callCount === 1) { + return new Response('tool_calls are not supported', { + status: 400, + headers: { 'Content-Type': 'text/plain' }, + }) + } + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'qwen2.5-coder:7b', + choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + tools: [{ + name: 'Read', + description: 'Read a file', + input_schema: { type: 'object', properties: { filePath: { type: 'string' } } }, + }], + max_tokens: 64, + stream: false, + }) + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-a']) +}) + test('Shim self-heals a JSON `tool_stream` rejection by retrying without it (#1950)', async () => { process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' process.env.OPENAI_API_KEY = 'sk-zai-test' @@ -1853,12 +1896,7 @@ test('GitHub Copilot 401 chat_completions with providerOverride does not trigger } }) -// --- JSON fallback regression tests (#1749) ------------------------------- -// Some OpenAI-compatible providers ignore `stream: true` and return a full -// `application/json` chat completion. The fallback inside -// openaiStreamToAnthropic must route that response through the same -// non-streaming converter so tool_calls, Anthropic stop reasons, array -// content, and stripping are all preserved (jatmn CHANGES_REQUESTED). +// --- retry helper unit tests ------------------------------------------------ test('formats retry guidance from server response headers', () => { expect(formatRetryAfterHint(new Response(null, { headers: { 'retry-after': '12' } }))) From 7638a36e086be3938180a90b50948f556cbb1483 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 09:19:54 -0700 Subject: [PATCH 15/25] test(openai-shim): assert redacted HTTP errors --- .../api/openaiShim/requestExecutor.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index b6cf8b04ac..3937d9d124 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -945,14 +945,20 @@ test('redacts configured secrets from HTTP error messages', async () => { const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ + const error = await client.beta.messages + .create({ model: 'test-model', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: false, - }), - ).rejects.not.toThrow('super-secret-key') + }) + .then( + () => undefined, + (caught: unknown) => caught as Error, + ) + + expect(error?.message).toContain('upstream rejected') + expect(error?.message).not.toContain('super-secret-key') }) test('classifies localhost transport failures with actionable category marker', async () => { From 9bdb77675b41022ee874318efe2c8b630928229f Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 09:27:42 -0700 Subject: [PATCH 16/25] test(openai-shim): isolate Azure executor configuration --- src/services/api/openaiShim/requestExecutor.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index 3937d9d124..786a399a71 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -22,6 +22,7 @@ const originalEnv = { OPENAI_API_KEYS: process.env.OPENAI_API_KEYS, OPENAI_MODEL: process.env.OPENAI_MODEL, OPENAI_API_FORMAT: process.env.OPENAI_API_FORMAT, + OPENAI_AZURE_STYLE: process.env.OPENAI_AZURE_STYLE, OPENAI_AUTH_HEADER: process.env.OPENAI_AUTH_HEADER, OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE, @@ -442,6 +443,7 @@ beforeEach(async () => { delete process.env.OPENAI_API_KEYS delete process.env.OPENAI_MODEL delete process.env.OPENAI_API_FORMAT + delete process.env.OPENAI_AZURE_STYLE delete process.env.OPENAI_AUTH_HEADER delete process.env.OPENAI_AUTH_SCHEME delete process.env.OPENAI_AUTH_HEADER_VALUE @@ -486,6 +488,7 @@ afterEach(() => { restoreEnv('OPENAI_API_KEYS', originalEnv.OPENAI_API_KEYS) restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL) restoreEnv('OPENAI_API_FORMAT', originalEnv.OPENAI_API_FORMAT) + restoreEnv('OPENAI_AZURE_STYLE', originalEnv.OPENAI_AZURE_STYLE) restoreEnv('OPENAI_AUTH_HEADER', originalEnv.OPENAI_AUTH_HEADER) restoreEnv('OPENAI_AUTH_SCHEME', originalEnv.OPENAI_AUTH_SCHEME) restoreEnv('OPENAI_AUTH_HEADER_VALUE', originalEnv.OPENAI_AUTH_HEADER_VALUE) From c0d8eb5677bda55df98719539e0b0e392391eced Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 11:02:58 -0700 Subject: [PATCH 17/25] fix(openai-shim): preserve executor recovery retries --- .../api/openaiShim/requestExecutor.test.ts | 60 ++++++++++++++++++- .../api/openaiShim/requestExecutor.ts | 8 ++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index 786a399a71..025027305d 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -1142,7 +1142,10 @@ test('self-heals localhost resolution failures by retrying local loopback base U }) test('self-heals tool-call incompatibility by retrying local Ollama requests without tools', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + // A direct loopback address has no alternate retry base, so this verifies + // the recovery reserves its own second request rather than relying on URL + // fallback attempts. + process.env.OPENAI_BASE_URL = 'http://127.0.0.1:11434/v1' const requestBodies: Array> = [] globalThis.fetch = (async (_input, init) => { @@ -1905,6 +1908,61 @@ test('GitHub Copilot 401 chat_completions with providerOverride does not trigger } }) +test('GitHub Copilot refreshes a matching providerOverride token', async () => { + const realGithubModule = realGithubModelsCredentials + try { + const refreshSpy = mock(async () => { + process.env.GITHUB_TOKEN = 'refreshed-token' + process.env.OPENAI_API_KEY = 'refreshed-token' + return true + }) + mock.module('../../../utils/githubModelsCredentials.js', () => ({ + ...realGithubModule, + refreshCopilotTokenOn401: refreshSpy, + })) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' + + const authorizations: string[] = [] + globalThis.fetch = ((_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? '') + if (authorizations.length === 1) { + return Promise.resolve(new Response( + JSON.stringify({ error: { message: 'token expired' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + )) + } + return Promise.resolve(makeChatCompletionResponse('gpt-4')) + }) as unknown as typeof globalThis.fetch + + const { createOpenAIShimClient: createClient } = + await importFreshOpenAIShim('copilot-401-matching-override') + const client = createClient({ + providerOverride: { + model: 'gpt-4', + baseURL: 'https://api.githubcopilot.com', + apiKey: 'initial-token', + }, + }) as OpenAIShimClient + + await expect(client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + })).resolves.toBeDefined() + + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(authorizations).toEqual(['Bearer initial-token', 'Bearer refreshed-token']) + } finally { + mock.module('../../../utils/githubModelsCredentials.js', () => realGithubModule) + } +}) + // --- retry helper unit tests ------------------------------------------------ test('formats retry guidance from server response headers', () => { diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 59a95a4b15..1f7071367b 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -597,9 +597,11 @@ export async function executeOpenAIRequest( const maxSelfHealAttempts = isLocal ? localRetryBaseUrls.length + 1 : 0 const credentialPoolAttempts = credentialPool?.size ?? 1 - let maxAttempts = + let maxAttempts = Math.max( + 2, Math.max(isGithub ? GITHUB_429_MAX_RETRIES : 1, credentialPoolAttempts) + - maxSelfHealAttempts + maxSelfHealAttempts, + ) const throwClassifiedTransportError = ( error: unknown, @@ -954,7 +956,7 @@ export async function executeOpenAIRequest( didRefreshCopilotToken = true const refreshed = await refreshCopilotTokenOn401() if (refreshed) { - const newApiKey = requestProcessEnv.OPENAI_API_KEY?.trim() || '' + const newApiKey = process.env.OPENAI_API_KEY?.trim() || '' if (newApiKey && newApiKey !== oldToken) { refreshedCopilotToken = newApiKey } From 368b5f33da33db570722e191e079ecad08384a02 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 11:59:55 -0700 Subject: [PATCH 18/25] test(openai-shim): remove migrated executor duplicates --- src/services/api/openaiShim.test.ts | 12917 ++++++++------------------ 1 file changed, 3638 insertions(+), 9279 deletions(-) diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index e532921809..27718f87b2 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -586,115 +586,6 @@ afterEach(() => { releaseSharedMutationLock() } }) - -// openaiShim test extraction seam 001 start: strips canonical Anthropic headers from direct shim defaultHeaders - -// openaiShim test extraction seam 001 end - - -// openaiShim test extraction seam 002 start: uses OpenAI-compatible responses endpoint when OPENAI_API_FORMAT=responses -test('uses OpenAI-compatible responses endpoint when OPENAI_API_FORMAT=responses', async () => { - process.env.OPENAI_API_FORMAT = 'responses' - let capturedUrl = '' - let capturedBody: Record | undefined - - globalThis.fetch = (async (input, init) => { - capturedUrl = String(input) - capturedBody = JSON.parse(String(init?.body)) as Record - - return new Response( - JSON.stringify({ - id: 'resp-1', - model: 'gpt-5.4', - output: [ - { - type: 'message', - role: 'assistant', - content: [{ type: 'output_text', text: 'ok' }], - }, - ], - usage: { - input_tokens: 8, - output_tokens: 3, - total_tokens: 11, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'gpt-5.4', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(capturedUrl).toBe('http://example.test/v1/responses') - expect(capturedBody?.model).toBe('gpt-5.4') - expect(capturedBody?.instructions).toBe('test system') - expect(capturedBody?.max_output_tokens).toBe(64) - expect(capturedBody?.store).toBe(false) - expect(capturedBody?.input).toEqual([ - { - type: 'message', - role: 'user', - content: [{ type: 'input_text', text: 'hello' }], - }, - ]) -}) -// openaiShim test extraction seam 002 end - - -// openaiShim test extraction seam 003 start: nests reasoning effort for OpenAI-compatible responses endpoint -test('nests reasoning effort for OpenAI-compatible responses endpoint', async () => { - process.env.OPENAI_API_FORMAT = 'responses' - let capturedBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - capturedBody = JSON.parse(String(init?.body)) as Record - - return new Response( - JSON.stringify({ - id: 'resp-1', - model: 'gpt-5.4', - output: [ - { - type: 'message', - role: 'assistant', - content: [{ type: 'output_text', text: 'ok' }], - }, - ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ reasoningEffort: 'high' }) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'gpt-5.4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(capturedBody?.reasoning).toEqual({ effort: 'high', summary: 'auto' }) - expect(capturedBody?.include).toEqual(['reasoning.encrypted_content']) - expect(capturedBody).not.toHaveProperty('reasoning_effort') - expect(capturedBody).not.toHaveProperty('reasoning_summary') -}) // openaiShim test extraction seam 003 end test('auto-routes gpt-5.6 to /responses on api.openai.com with tools and nested reasoning', async () => { @@ -1262,65 +1153,6 @@ test('auto-routed gpt-5.6 on an Azure base nests reasoning.effort and the encryp expect(capturedBody?.reasoning).toEqual({ effort: 'high', summary: 'auto' }) expect(capturedBody?.include).toEqual(['reasoning.encrypted_content']) }) - -// openaiShim test extraction seam 004 start: uses OpenAI-compatible responses endpoint with text chunk types when OPENAI_API_FORMAT=responses_compat -test('uses OpenAI-compatible responses endpoint with text chunk types when OPENAI_API_FORMAT=responses_compat', async () => { - process.env.OPENAI_API_FORMAT = 'responses_compat' - let capturedUrl = '' - let capturedBody: Record | undefined - - globalThis.fetch = (async (input, init) => { - capturedUrl = String(input) - capturedBody = JSON.parse(String(init?.body)) as Record - - return new Response( - JSON.stringify({ - id: 'resp-1', - model: 'gpt-5.4', - output: [ - { - type: 'message', - role: 'assistant', - content: [{ type: 'output_text', text: 'ok' }], - }, - ], - usage: { - input_tokens: 8, - output_tokens: 3, - total_tokens: 11, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'gpt-5.4', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(capturedUrl).toBe('http://example.test/v1/responses') - expect(capturedBody?.model).toBe('gpt-5.4') - expect(capturedBody?.instructions).toBe('test system') - expect(capturedBody?.max_output_tokens).toBe(64) - expect(capturedBody?.store).toBe(false) - expect(capturedBody?.input).toEqual([ - { - type: 'message', - role: 'user', - content: [{ type: 'text', text: 'hello' }], - }, - ]) -}) // openaiShim test extraction seam 004 end @@ -1369,838 +1201,495 @@ test('uses correct empty input fallback schema for standard responses and respon }, ]) }) - -// openaiShim test extraction seam 005 end +// openaiShim test extraction seam 020 end -// openaiShim test extraction seam 006 start: strips store from strict OpenAI-compatible responses providers -test('strips store from strict OpenAI-compatible responses providers', async () => { - process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' - process.env.OPENAI_API_FORMAT = 'responses' - let capturedUrl = '' - let capturedBody: Record | undefined +// openaiShim test extraction seam 021 start: preserves usage from final OpenAI stream chunk with empty choices +test('preserves usage from final OpenAI stream chunk with empty choices', async () => { + globalThis.fetch = (async (_input, init) => { + const url = typeof _input === 'string' ? _input : _input.url + expect(url).toBe('http://example.test/v1/chat/completions') - globalThis.fetch = (async (input, init) => { - capturedUrl = String(input) - capturedBody = JSON.parse(String(init?.body)) as Record + const body = JSON.parse(String(init?.body)) + expect(body.stream).toBe(true) + expect(body.stream_options).toEqual({ include_usage: true }) - return new Response( - JSON.stringify({ - id: 'resp-1', - model: 'kimi-k2.5', - output: [ + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'fake-model', + choices: [ { - type: 'message', - role: 'assistant', - content: [{ type: 'output_text', text: 'ok' }], + index: 0, + delta: { role: 'assistant', content: 'hello world' }, + finish_reason: null, }, ], - }), + }, { - headers: { - 'Content-Type': 'application/json', + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'fake-model', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'stop', + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'fake-model', + choices: [], + usage: { + prompt_tokens: 123, + completion_tokens: 45, + total_tokens: 168, }, }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'kimi-k2.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(capturedUrl).toBe('https://api.moonshot.ai/v1/responses') - expect(capturedBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 006 end + ]) + return makeSseResponse(chunks) + }) as unknown as FetchType -// openaiShim test extraction seam 007 start: strips store when providerOverride routes chat_completions to the Gemini host -test('strips store when providerOverride routes chat_completions to the Gemini host', async () => { - let capturedBody: Record | undefined + const client = createOpenAIShimClient({}) as OpenAIShimClient - globalThis.fetch = (async (_input, init) => { - capturedBody = JSON.parse(String(init?.body)) as Record - return new Response( - JSON.stringify({ - id: 'chatcmpl-gemini', - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType + const result = await client.beta.messages + .create({ + model: 'fake-model', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const client = createOpenAIShimClient({ - defaultHeaders: {}, - providerOverride: { - model: 'gemini-3.1-pro', - baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', - apiKey: 'gemini-key', - }, - }) as OpenAIShimClient + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - await client.beta.messages.create({ - model: 'gemini-3.1-pro', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) + const usageEvent = events.find( + event => event.type === 'message_delta' && typeof event.usage === 'object' && event.usage !== null, + ) as { usage?: { input_tokens?: number; output_tokens?: number } } | undefined - expect(capturedBody?.store).toBeUndefined() + expect(usageEvent).toBeDefined() + expect(usageEvent?.usage?.input_tokens).toBe(123) + expect(usageEvent?.usage?.output_tokens).toBe(45) }) -// openaiShim test extraction seam 007 end +// openaiShim test extraction seam 021 end -// openaiShim test extraction seam 008 start: strips store when providerOverride routes responses API to the Gemini host -test('strips store when providerOverride routes responses API to the Gemini host', async () => { - process.env.OPENAI_API_FORMAT = 'responses' - let capturedBody: Record | undefined +// Extraction seam: stream conversion usage | shared stream control. - globalThis.fetch = (async (_input, init) => { - capturedBody = JSON.parse(String(init?.body)) as Record - return new Response( - JSON.stringify({ - id: 'resp-gemini', - output: [ - { - type: 'message', - role: 'assistant', - content: [{ type: 'output_text', text: 'ok' }], - }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - defaultHeaders: {}, - providerOverride: { - model: 'gemini-3.1-pro', - baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', - apiKey: 'gemini-key', +// openaiShim test extraction seam 022 start: readWithIdleTimeout rejects quickly and cancels a stalled reader +test('readWithIdleTimeout rejects quickly and cancels a stalled reader', async () => { + const testApi = await getStreamIdleTestApi('stream-idle-helper') + const cancelReasons: unknown[] = [] + const reader = new ReadableStream({ + cancel(reason) { + cancelReasons.push(reason) }, - }) as OpenAIShimClient + }).getReader() - await client.beta.messages.create({ - model: 'gemini-3.1-pro', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) + const startedAt = Date.now() + let caught: unknown + try { + await testApi.readWithIdleTimeout(reader, 20) + } catch (error) { + caught = error + } - expect(capturedBody?.store).toBeUndefined() + expect(Date.now() - startedAt).toBeLessThan(500) + expect(caught).toBeInstanceOf(testApi.StreamIdleTimeoutError) + expect((caught as Error).name).toBe('StreamIdleTimeoutError') + expect(cancelReasons).toHaveLength(1) + expect(cancelReasons[0]).toBeInstanceOf(testApi.StreamIdleTimeoutError) }) -// openaiShim test extraction seam 008 end - - -// openaiShim test extraction seam 009 start: uses custom OpenAI-compatible auth header value when configured -test('uses custom OpenAI-compatible auth header value when configured', async () => { - process.env.OPENAI_API_KEY = 'generic-key' - process.env.OPENAI_AUTH_HEADER = 'api-key' - process.env.OPENAI_AUTH_HEADER_VALUE = 'hicap-header-value' - let capturedHeaders: Headers | undefined - - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers as HeadersInit) +// openaiShim test extraction seam 022 end - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient +// openaiShim test extraction seam 023 start: readWithIdleTimeout preserves parent abort instead of reporting idle timeout +test('readWithIdleTimeout preserves parent abort instead of reporting idle timeout', async () => { + const testApi = await getStreamIdleTestApi('stream-idle-user-abort') + const parent = new AbortController() + const cancelReasons: unknown[] = [] + const reader = new ReadableStream({ + cancel(reason) { + cancelReasons.push(reason) + }, + }).getReader() - await client.beta.messages.create({ - model: 'gpt-4o', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, + const read = testApi.readWithIdleTimeout(reader, 1_000, { + signal: parent.signal, }) + parent.abort() - expect(capturedHeaders?.get('api-key')).toBe('hicap-header-value') - expect(capturedHeaders?.get('authorization')).toBeNull() -}) -// openaiShim test extraction seam 009 end + let caught: unknown + try { + await read + } catch (error) { + caught = error + } + expect(caught).toBeInstanceOf(DOMException) + expect((caught as DOMException).name).toBe('AbortError') + expect(cancelReasons).toHaveLength(1) + expect(cancelReasons[0]).toBeInstanceOf(DOMException) + expect((cancelReasons[0] as DOMException).name).toBe('AbortError') +}) +// openaiShim test extraction seam 023 end -// openaiShim test extraction seam 010 start: uses Hicap api-key auth header for the Hicap route -test('uses Hicap api-key auth header for the Hicap route', async () => { - process.env.OPENAI_API_KEY = 'hicap-live-key' - process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' - let capturedHeaders: Headers | undefined - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers as HeadersInit) +// openaiShim test extraction seam 024 start: stream idle timeout env parser parses and bounds overrides +test('stream idle timeout env parser parses and bounds overrides', async () => { + const testApi = await getStreamIdleTestApi('stream-idle-env-parser') - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType + delete process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS + expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' + expect(testApi.getStreamIdleTimeoutMs()).toBe(25) - await client.beta.messages.create({ - model: 'claude-opus-4.8', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = ' 25 ' + expect(testApi.getStreamIdleTimeoutMs()).toBe(25) - expect(capturedHeaders?.get('api-key')).toBe('hicap-live-key') - expect(capturedHeaders?.get('authorization')).toBeNull() -}) -// openaiShim test extraction seam 010 end + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '3000000000' + expect(testApi.getStreamIdleTimeoutMs()).toBe(2_147_483_647) + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '9007199254740993' + expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) -// openaiShim test extraction seam 011 start: defaults Authorization custom auth header to bearer scheme -test('defaults Authorization custom auth header to bearer scheme', async () => { - process.env.OPENAI_API_KEY = 'authorization-key' - process.env.OPENAI_AUTH_HEADER = 'Authorization' - let capturedHeaders: Headers | undefined + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25ms' + expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers as HeadersInit) + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '0' + expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '-5' + expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) +}) +// openaiShim test extraction seam 024 end - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient +test('API timeout env parser accepts safe positive integers and falls back otherwise', async () => { + const testApi = await getStreamIdleTestApi('api-timeout-env-parser') - await client.beta.messages.create({ - model: 'gpt-4o', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) + delete process.env.API_TIMEOUT_MS + expect(testApi.getApiTimeoutMs()).toBe(600_000) - expect(capturedHeaders?.get('authorization')).toBe('Bearer authorization-key') -}) -// openaiShim test extraction seam 011 end + process.env.API_TIMEOUT_MS = '50' + expect(testApi.getApiTimeoutMs()).toBe(50) + process.env.API_TIMEOUT_MS = ' 50 ' + expect(testApi.getApiTimeoutMs()).toBe(50) -// openaiShim test extraction seam 012 start: honors bearer scheme for custom OpenAI-compatible auth headers -test('honors bearer scheme for custom OpenAI-compatible auth headers', async () => { - process.env.OPENAI_API_KEY = 'custom-key' - process.env.OPENAI_AUTH_HEADER = 'X-Custom-Authorization' - process.env.OPENAI_AUTH_SCHEME = 'bearer' - let capturedHeaders: Headers | undefined + process.env.API_TIMEOUT_MS = '3000000000' + expect(testApi.getApiTimeoutMs()).toBe(2_147_483_647) - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers as HeadersInit) + for (const invalid of ['abc', '-5', '', '0', '1.5', '9007199254740993']) { + process.env.API_TIMEOUT_MS = invalid + expect(testApi.getApiTimeoutMs()).toBe(600_000) + } +}) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }), - { - headers: { - 'Content-Type': 'application/json', - }, +// openaiShim test extraction seam 025 start: Anthropic-compatible passthrough stream rejects with idle timeout when it stalls +test('Anthropic-compatible passthrough stream rejects with idle timeout when it stalls', async () => { + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' + const stalled = makeStallingResponse( + `data: ${JSON.stringify({ + type: 'message_start', + message: { + id: 'msg_idle_passthrough', + type: 'message', + role: 'assistant', + content: [], + model: 'passthrough-model', + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 0, output_tokens: 0 }, }, - ) - }) as unknown as FetchType + })}\n\n`, + 'https://api.anthropic-shaped.example.com/v1/messages', + ) - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + globalThis.fetch = (async () => stalled.response) as unknown as FetchType - await client.beta.messages.create({ - model: 'gpt-4o', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'passthrough-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - expect(capturedHeaders?.get('x-custom-authorization')).toBe('Bearer custom-key') - expect(capturedHeaders?.get('authorization')).toBeNull() -}) -// openaiShim test extraction seam 012 end + let caught: unknown + try { + for await (const _event of result.data) { + // drain until the stalled reader times out + } + } catch (error) { + caught = error + } finally { + stalled.close() + } + expect((caught as Error).name).toBe('StreamIdleTimeoutError') + expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') +}) +// openaiShim test extraction seam 025 end -// openaiShim test extraction seam 013 start: ignores custom auth header value when no custom header is configured -test('ignores custom auth header value when no custom header is configured', async () => { - delete process.env.OPENAI_API_KEY - process.env.OPENAI_AUTH_HEADER_VALUE = 'gateway-header-value' - let capturedHeaders: Headers | undefined - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers as HeadersInit) +// Extraction seam: shared stream control | Gemini stream conversion. - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - choices: [{ message: { role: 'assistant', content: 'ok' } }], - }), - { - headers: { - 'Content-Type': 'application/json', +// openaiShim test extraction seam 026 start: Gemini SSE stream rejects with idle timeout when it stalls +test('Gemini SSE stream rejects with idle timeout when it stalls', async () => { + process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' + const stalled = makeStallingResponse( + `data: ${JSON.stringify({ + candidates: [ + { + content: { + role: 'model', + parts: [{ text: 'partial' }], + }, }, - }, - ) - }) as unknown as FetchType + ], + })}\n\n`, + 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent', + ) - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient + globalThis.fetch = (async () => stalled.response) as unknown as FetchType - await client.beta.messages.create({ - model: 'gpt-4o', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'google/gemini-2.5-pro', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - expect(capturedHeaders?.get('authorization')).toBeNull() -}) -// openaiShim test extraction seam 013 end + let caught: unknown + try { + for await (const _event of result.data) { + // drain until the stalled reader times out + } + } catch (error) { + caught = error + } finally { + stalled.close() + } + expect((caught as Error).name).toBe('StreamIdleTimeoutError') + expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') +}) +// openaiShim test extraction seam 028 end -// openaiShim test extraction seam 014 start: strips canonical Anthropic headers from per-request shim headers too -// openaiShim test extraction seam 014 end +// openaiShim test extraction seam 029 start: controller abort reaches generic OpenAI SSE converter +test('controller abort reaches generic OpenAI SSE converter', async () => { + const stalled = makeStallingResponse( + makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), + ) + globalThis.fetch = (async () => stalled.response) as unknown as FetchType -// openaiShim test extraction seam 015 start: applies descriptor static headers before client and request headers -test('applies descriptor static headers before client and request headers', async () => { - let capturedHeaders: Headers | undefined + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'fake-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + const stream = result.data as unknown as ShimStream - registerGateway({ - id: 'shim-header-test', - label: 'Shim Header Test', - category: 'hosted', - defaultBaseUrl: 'https://shim-header-test.example/v1', - defaultModel: 'shim-test-model', - setup: { - requiresAuth: true, - authMode: 'api-key', - credentialEnvVars: ['OPENAI_API_KEY'], - }, - transportConfig: { - kind: 'openai-compatible', - openaiShim: { - headers: { - 'x-static-header': 'from-descriptor', - 'x-override-header': 'from-descriptor', - }, - }, - }, - }) + try { + const outcome = await expectAbortStopsStream({ + abort: () => stream.controller.abort(), + cancelReasons: stalled.cancelReasons, + expectedEventsBeforeAbort: 3, + label: 'generic OpenAI SSE stream', + stream, + }) - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://shim-header-test.example/v1' - process.env.OPENAI_MODEL = 'shim-test-model' + expect(outcome.events.some(event => event.type === 'content_block_delta')).toBe(true) + } finally { + stalled.close() + } +}) +// openaiShim test extraction seam 029 end - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'shim-test-model', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 8, - completion_tokens: 3, - total_tokens: 11, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType +// openaiShim test extraction seam 030 start: controller abort cancels generic OpenAI SSE before iteration starts +test('controller abort cancels generic OpenAI SSE before iteration starts', async () => { + const stalled = makeStallingResponse( + makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), + ) - const client = createOpenAIShimClient({ - defaultHeaders: { - 'x-override-header': 'from-client', - }, - }) as OpenAIShimClient + globalThis.fetch = (async () => stalled.response) as unknown as FetchType - await client.beta.messages.create( - { - model: 'shim-test-model', - system: 'test system', + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'fake-model', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, - stream: false, - }, - { - headers: { - 'x-override-header': 'from-request', - }, - }, - ) + stream: true, + }) + .withResponse() + const stream = result.data as unknown as ShimStream - expect(capturedHeaders?.get('x-static-header')).toBe('from-descriptor') - expect(capturedHeaders?.get('x-override-header')).toBe('from-request') + try { + stream.controller.abort() + await waitForPromise( + (async () => { + for (let i = 0; i < 10; i++) { + if (stalled.cancelReasons.length > 0) return + await Promise.resolve() + } + throw new Error('pre-iteration OpenAI SSE stream did not cancel source') + })(), + 500, + 'pre-iteration OpenAI SSE stream did not cancel source', + ) + expect(stalled.cancelReasons).toHaveLength(1) + } finally { + stalled.close() + } }) -// openaiShim test extraction seam 015 end - - -// openaiShim test extraction seam 016 start: opengateway sends Accept-Encoding: identity header on chat requests -test('opengateway sends Accept-Encoding: identity header on chat requests', async () => { - let capturedHeaders: Headers | undefined - - registerGateway({ - id: 'gitlawb-opengateway-test', - label: 'Gitlawb Opengateway', - category: 'aggregating', - defaultBaseUrl: 'https://opengateway.gitlawb.com/v1/xiaomi-mimo', - defaultModel: 'mimo-v2.5-pro', - setup: { - requiresAuth: false, - authMode: 'none', - }, - transportConfig: { - kind: 'openai-compatible', - openaiShim: { - headers: { - 'Accept-Encoding': 'identity', - }, - defaultAuthHeader: { - name: 'api-key', - scheme: 'raw', - }, - preserveReasoningContent: true, - requireReasoningContentOnAssistantMessages: true, - reasoningContentFallback: '', - maxTokensField: 'max_completion_tokens', - supportsApiFormatSelection: false, - supportsAuthHeaders: false, - }, - }, - }) +// openaiShim test extraction seam 030 end - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1/xiaomi-mimo' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) +// openaiShim test extraction seam 031 start: controller abort cancels generic OpenAI SSE when paused after message_start +test('controller abort cancels generic OpenAI SSE when paused after message_start', async () => { + const stalled = makeStallingResponse( + makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), + ) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'mimo-v2.5-pro', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 8, - completion_tokens: 3, - total_tokens: 11, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType + globalThis.fetch = (async () => stalled.response) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create( - { - model: 'mimo-v2.5-pro', - system: 'test system', + const result = await client.beta.messages + .create({ + model: 'fake-model', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, - stream: false, - }, - {}, - ) + stream: true, + }) + .withResponse() + const stream = result.data as unknown as ShimStream - expect(capturedHeaders?.get('Accept-Encoding')).toBe('identity') + try { + await expectPausedAbortCancelsStream({ + cancelReasons: stalled.cancelReasons, + label: 'paused generic OpenAI SSE stream', + stream, + }) + } finally { + stalled.close() + } }) -// openaiShim test extraction seam 016 end - - -// openaiShim test extraction seam 017 start: strips Anthropic-specific headers on GitHub Codex transport requests -test('strips Anthropic-specific headers on GitHub Codex transport requests', async () => { - let capturedHeaders: Headers | undefined +// openaiShim test extraction seam 031 end - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_API_KEY = 'github-test-key' - process.env.GITHUB_TOKEN = 'stored-secret' - delete process.env.GITHUB_COPILOT_KEY - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_MODEL - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) +// openaiShim test extraction seam 032 start: controller abort stops buffered generic OpenAI SSE events +test('controller abort stops buffered generic OpenAI SSE events', async () => { + const stalled = makeStallingResponse( + makeOpenAIStreamFrame({ role: 'assistant', content: 'first' }) + + makeOpenAIStreamFrame({ content: 'second' }), + ) - return new Response('', { - status: 200, - headers: { - 'Content-Type': 'text/event-stream', - }, - }) - }) as unknown as FetchType + globalThis.fetch = (async () => stalled.response) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create( - { - model: 'github:gpt-5-codex', - system: 'test system', + const result = await client.beta.messages + .create({ + model: 'fake-model', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, - }, - { - headers: { - 'anthropic-version': '2023-06-01', - 'anthropic-beta': 'prompt-caching-2024-07-31', - 'x-anthropic-additional-protection': 'true', - 'x-safe-header': 'keep-me', - }, - }, - ) + }) + .withResponse() + const stream = result.data as unknown as ShimStream - expect(capturedHeaders?.get('anthropic-version')).toBeNull() - expect(capturedHeaders?.get('anthropic-beta')).toBeNull() - expect(capturedHeaders?.get('x-anthropic-additional-protection')).toBeNull() - expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me') - expect(capturedHeaders?.get('authorization')).toBe('Bearer github-test-key') - expect(capturedHeaders?.get('editor-plugin-version')).toBe('copilot-chat/0.26.7') + try { + await expectBufferedAbortRejectsNext({ + expectedText: 'first', + label: 'buffered generic OpenAI SSE stream', + stream, + }) + expect(stalled.cancelReasons).toHaveLength(1) + } finally { + stalled.close() + } }) -// openaiShim test extraction seam 017 end - +// openaiShim test extraction seam 032 end -// openaiShim test extraction seam 018 start: uses direct GitHub Copilot Enterprise key for shim authentication -test('uses direct GitHub Copilot Enterprise key for shim authentication', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.GITHUB_COPILOT_KEY = 'enterprise-direct-key' - process.env.GITHUB_ENTERPRISE_URL = 'https://github.mycompany.com' - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL - const { authorization, url } = await captureChatCompletionRequest( - 'github:gpt-4o', +// openaiShim test extraction seam 033 start: controller abort reaches Anthropic messages SSE passthrough +test('controller abort reaches Anthropic messages SSE passthrough', async () => { + const stalled = makeStallingResponse( + `data: ${JSON.stringify({ + type: 'message_start', + message: { + id: 'msg_passthrough_abort', + type: 'message', + role: 'assistant', + content: [], + model: 'passthrough-model', + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 0, output_tokens: 0 }, + }, + })}\n\n`, + 'https://api.anthropic-shaped.example.com/v1/messages', ) - expect(authorization).toBe('Bearer enterprise-direct-key') - expect(url).toBe('https://github.mycompany.com/api/copilot/chat/completions') -}) -// openaiShim test extraction seam 018 end - - -// openaiShim test extraction seam 019 start: direct GitHub Copilot key wins over stale OpenAI key -test('direct GitHub Copilot key wins over stale OpenAI key', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.GITHUB_COPILOT_KEY = 'enterprise-direct-key' - process.env.GITHUB_ENTERPRISE_URL = 'https://github.mycompany.com' - process.env.OPENAI_API_KEY = 'stale-openai-key' - delete process.env.OPENAI_BASE_URL - - const { authorization } = await captureChatCompletionRequest( - 'github:gpt-4o', - ) - - expect(authorization).toBe('Bearer enterprise-direct-key') -}) -// openaiShim test extraction seam 019 end - - -// openaiShim test extraction seam 020 start: strips Anthropic-specific headers on GitHub Codex transport with providerOverride API key -test('strips Anthropic-specific headers on GitHub Codex transport with providerOverride API key', async () => { - let capturedHeaders: Headers | undefined - - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_API_KEY = 'env-should-not-win' - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_MODEL - - globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) - - return new Response('', { - status: 200, - headers: { - 'Content-Type': 'text/event-stream', - }, - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - providerOverride: { - model: 'github:gpt-5-codex', - baseURL: 'https://api.githubcopilot.com', - apiKey: 'provider-override-key', - }, - }) as OpenAIShimClient - - await client.beta.messages.create( - { - model: 'ignored', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }, - { - headers: { - 'anthropic-version': '2023-06-01', - 'x-claude-remote-session-id': 'remote-123', - 'x-safe-header': 'keep-me', - }, - }, - ) - - expect(capturedHeaders?.get('anthropic-version')).toBeNull() - expect(capturedHeaders?.get('x-claude-remote-session-id')).toBeNull() - expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me') - expect(capturedHeaders?.get('authorization')).toBe('Bearer provider-override-key') - expect(capturedHeaders?.get('editor-plugin-version')).toBe('copilot-chat/0.26.7') -}) -// openaiShim test extraction seam 020 end - - -// openaiShim test extraction seam 021 start: preserves usage from final OpenAI stream chunk with empty choices -test('preserves usage from final OpenAI stream chunk with empty choices', async () => { - globalThis.fetch = (async (_input, init) => { - const url = typeof _input === 'string' ? _input : _input.url - expect(url).toBe('http://example.test/v1/chat/completions') - - const body = JSON.parse(String(init?.body)) - expect(body.stream).toBe(true) - expect(body.stream_options).toEqual({ include_usage: true }) - - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'fake-model', - choices: [ - { - index: 0, - delta: { role: 'assistant', content: 'hello world' }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'fake-model', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'stop', - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'fake-model', - choices: [], - usage: { - prompt_tokens: 123, - completion_tokens: 45, - total_tokens: 168, - }, - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType + globalThis.fetch = (async () => stalled.response) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages .create({ - model: 'fake-model', - system: 'test system', + model: 'passthrough-model', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, }) .withResponse() + const stream = result.data as unknown as ShimStream - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const usageEvent = events.find( - event => event.type === 'message_delta' && typeof event.usage === 'object' && event.usage !== null, - ) as { usage?: { input_tokens?: number; output_tokens?: number } } | undefined - - expect(usageEvent).toBeDefined() - expect(usageEvent?.usage?.input_tokens).toBe(123) - expect(usageEvent?.usage?.output_tokens).toBe(45) -}) -// openaiShim test extraction seam 021 end - - -// Extraction seam: stream conversion usage | shared stream control. - -// openaiShim test extraction seam 022 start: readWithIdleTimeout rejects quickly and cancels a stalled reader -test('readWithIdleTimeout rejects quickly and cancels a stalled reader', async () => { - const testApi = await getStreamIdleTestApi('stream-idle-helper') - const cancelReasons: unknown[] = [] - const reader = new ReadableStream({ - cancel(reason) { - cancelReasons.push(reason) - }, - }).getReader() - - const startedAt = Date.now() - let caught: unknown try { - await testApi.readWithIdleTimeout(reader, 20) - } catch (error) { - caught = error - } - - expect(Date.now() - startedAt).toBeLessThan(500) - expect(caught).toBeInstanceOf(testApi.StreamIdleTimeoutError) - expect((caught as Error).name).toBe('StreamIdleTimeoutError') - expect(cancelReasons).toHaveLength(1) - expect(cancelReasons[0]).toBeInstanceOf(testApi.StreamIdleTimeoutError) -}) -// openaiShim test extraction seam 022 end - - -// openaiShim test extraction seam 023 start: readWithIdleTimeout preserves parent abort instead of reporting idle timeout -test('readWithIdleTimeout preserves parent abort instead of reporting idle timeout', async () => { - const testApi = await getStreamIdleTestApi('stream-idle-user-abort') - const parent = new AbortController() - const cancelReasons: unknown[] = [] - const reader = new ReadableStream({ - cancel(reason) { - cancelReasons.push(reason) - }, - }).getReader() - - const read = testApi.readWithIdleTimeout(reader, 1_000, { - signal: parent.signal, - }) - parent.abort() + const outcome = await expectAbortStopsStream({ + abort: () => stream.controller.abort(), + cancelReasons: stalled.cancelReasons, + expectedEventsBeforeAbort: 1, + label: 'Anthropic messages passthrough stream', + stream, + }) - let caught: unknown - try { - await read - } catch (error) { - caught = error + expect(outcome.events[0]?.type).toBe('message_start') + } finally { + stalled.close() } - - expect(caught).toBeInstanceOf(DOMException) - expect((caught as DOMException).name).toBe('AbortError') - expect(cancelReasons).toHaveLength(1) - expect(cancelReasons[0]).toBeInstanceOf(DOMException) - expect((cancelReasons[0] as DOMException).name).toBe('AbortError') -}) -// openaiShim test extraction seam 023 end - - -// openaiShim test extraction seam 024 start: stream idle timeout env parser parses and bounds overrides -test('stream idle timeout env parser parses and bounds overrides', async () => { - const testApi = await getStreamIdleTestApi('stream-idle-env-parser') - - delete process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS - expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' - expect(testApi.getStreamIdleTimeoutMs()).toBe(25) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = ' 25 ' - expect(testApi.getStreamIdleTimeoutMs()).toBe(25) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '3000000000' - expect(testApi.getStreamIdleTimeoutMs()).toBe(2_147_483_647) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '9007199254740993' - expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25ms' - expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '0' - expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) - - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '-5' - expect(testApi.getStreamIdleTimeoutMs()).toBe(90_000) }) -// openaiShim test extraction seam 024 end - -test('API timeout env parser accepts safe positive integers and falls back otherwise', async () => { - const testApi = await getStreamIdleTestApi('api-timeout-env-parser') - - delete process.env.API_TIMEOUT_MS - expect(testApi.getApiTimeoutMs()).toBe(600_000) - - process.env.API_TIMEOUT_MS = '50' - expect(testApi.getApiTimeoutMs()).toBe(50) - - process.env.API_TIMEOUT_MS = ' 50 ' - expect(testApi.getApiTimeoutMs()).toBe(50) - - process.env.API_TIMEOUT_MS = '3000000000' - expect(testApi.getApiTimeoutMs()).toBe(2_147_483_647) +// openaiShim test extraction seam 033 end - for (const invalid of ['abc', '-5', '', '0', '1.5', '9007199254740993']) { - process.env.API_TIMEOUT_MS = invalid - expect(testApi.getApiTimeoutMs()).toBe(600_000) - } -}) -// openaiShim test extraction seam 025 start: Anthropic-compatible passthrough stream rejects with idle timeout when it stalls -test('Anthropic-compatible passthrough stream rejects with idle timeout when it stalls', async () => { - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' +// openaiShim test extraction seam 034 start: controller abort cancels Anthropic messages SSE when paused after event +test('controller abort cancels Anthropic messages SSE when paused after event', async () => { const stalled = makeStallingResponse( `data: ${JSON.stringify({ type: 'message_start', message: { - id: 'msg_idle_passthrough', + id: 'msg_paused_passthrough_abort', type: 'message', role: 'assistant', content: [], @@ -2224,41 +1713,48 @@ test('Anthropic-compatible passthrough stream rejects with idle timeout when it stream: true, }) .withResponse() + const stream = result.data as unknown as ShimStream - let caught: unknown try { - for await (const _event of result.data) { - // drain until the stalled reader times out - } - } catch (error) { - caught = error + await expectPausedAbortCancelsStream({ + cancelReasons: stalled.cancelReasons, + label: 'paused Anthropic messages passthrough stream', + stream, + }) } finally { stalled.close() } - - expect((caught as Error).name).toBe('StreamIdleTimeoutError') - expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') }) -// openaiShim test extraction seam 025 end - +// openaiShim test extraction seam 034 end -// Extraction seam: shared stream control | Gemini stream conversion. -// openaiShim test extraction seam 026 start: Gemini SSE stream rejects with idle timeout when it stalls -test('Gemini SSE stream rejects with idle timeout when it stalls', async () => { - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' - const stalled = makeStallingResponse( - `data: ${JSON.stringify({ - candidates: [ - { - content: { - role: 'model', - parts: [{ text: 'partial' }], - }, +// openaiShim test extraction seam 035 start: controller abort stops buffered Anthropic messages SSE events +test('controller abort stops buffered Anthropic messages SSE events', async () => { + const stalled = makeStallingResponse( + [ + `data: ${JSON.stringify({ + type: 'message_start', + message: { + id: 'msg_buffered_passthrough_abort', + type: 'message', + role: 'assistant', + content: [], + model: 'passthrough-model', + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 0, output_tokens: 0 }, }, - ], - })}\n\n`, - 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent', + })}`, + '', + `data: ${JSON.stringify({ + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + })}`, + '', + '', + ].join('\n'), + 'https://api.anthropic-shaped.example.com/v1/messages', ) globalThis.fetch = (async () => stalled.response) as unknown as FetchType @@ -2266,208 +1762,76 @@ test('Gemini SSE stream rejects with idle timeout when it stalls', async () => { const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages .create({ - model: 'google/gemini-2.5-pro', + model: 'passthrough-model', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, }) .withResponse() + const stream = result.data as unknown as ShimStream + const iterator = stream[Symbol.asyncIterator]() - let caught: unknown try { - for await (const _event of result.data) { - // drain until the stalled reader times out - } - } catch (error) { - caught = error - } finally { - stalled.close() - } - - expect((caught as Error).name).toBe('StreamIdleTimeoutError') - expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') -}) -// openaiShim test extraction seam 026 end - - -// openaiShim test extraction seam 027 start: OpenAI-compatible stream rejects with idle timeout when it stalls after a chunk -test('OpenAI-compatible stream rejects with idle timeout when it stalls after a chunk', async () => { - await getStreamIdleTestApi('stream-idle-openai-stall') - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' - const stalled = makeStallingResponse( - makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), - ) - - globalThis.fetch = (async () => stalled.response) as unknown as FetchType + const first = await waitForPromise( + iterator.next(), + 500, + 'buffered Anthropic messages passthrough did not produce first event', + ) + expect(first.done).toBe(false) + expect(first.value?.type).toBe('message_start') - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'glm-5.2', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() + stream.controller.abort() + const afterAbort = await waitForPromise( + iterator.next().then( + value => ({ status: 'resolved' as const, value }), + error => ({ status: 'rejected' as const, error }), + ), + 500, + 'buffered Anthropic messages passthrough did not stop after abort', + ) - const events: Array> = [] - const startedAt = Date.now() - let caught: unknown - try { - for await (const event of result.data) { - events.push(event) + if (afterAbort.status !== 'rejected') { + throw new Error(`buffered Anthropic messages passthrough yielded after abort: ${JSON.stringify(afterAbort.value)}`) } - } catch (error) { - caught = error + expect((afterAbort.error as { name?: unknown }).name).toBe('AbortError') + expect(stalled.cancelReasons).toHaveLength(1) } finally { + await Promise.resolve(iterator.return?.()).catch(() => {}) stalled.close() } - - expect(Date.now() - startedAt).toBeLessThan(500) - expect((caught as Error).name).toBe('StreamIdleTimeoutError') - expect((stalled.cancelReasons[0] as Error).name).toBe('StreamIdleTimeoutError') - const textDeltas = events.flatMap(event => { - const eventDelta = event.delta as { type?: string; text?: string } | undefined - return eventDelta?.type === 'text_delta' && typeof eventDelta.text === 'string' - ? [eventDelta.text] - : [] - }) - expect(textDeltas).toEqual(['partial']) -}) -// openaiShim test extraction seam 027 end - - -// openaiShim test extraction seam 028 start: OpenAI-compatible stream keeps slow active chunks alive under the idle timeout -test('OpenAI-compatible stream keeps slow active chunks alive under the idle timeout', async () => { - await getStreamIdleTestApi('stream-idle-openai-active') - process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '500' - const startedAt = Date.now() - const encoder = new TextEncoder() - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-active', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [ - { - index: 0, - delta: { role: 'assistant', content: 'hel' }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-active', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [ - { - index: 0, - delta: { content: 'lo' }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-active', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'stop', - }, - ], - }, - ]) - let emitTimer: ReturnType | undefined - - globalThis.fetch = asMockFetch(mock(async () => - new Response( - new ReadableStream({ - start(controller) { - let index = 0 - const emit = () => { - emitTimer = undefined - const chunk = chunks[index++] - if (chunk === undefined) { - controller.close() - return - } - controller.enqueue(encoder.encode(chunk)) - emitTimer = setTimeout(emit, 200) - } - emit() - }, - cancel() { - if (emitTimer !== undefined) { - clearTimeout(emitTimer) - emitTimer = undefined - } - }, - }), - { - headers: { - 'Content-Type': 'text/event-stream', - }, - }, - ))) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'glm-5.2', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const textDeltas: string[] = [] - for await (const event of result.data) { - const streamDelta = (event as { delta?: { type?: string; text?: string } }).delta - if ( - streamDelta?.type === 'text_delta' && - typeof streamDelta.text === 'string' - ) { - textDeltas.push(streamDelta.text) - } - } - - expect(Date.now() - startedAt).toBeGreaterThan(500) - expect(textDeltas.join('')).toBe('hello') }) -// openaiShim test extraction seam 028 end +// openaiShim test extraction seam 035 end -// openaiShim test extraction seam 029 start: controller abort reaches generic OpenAI SSE converter -test('controller abort reaches generic OpenAI SSE converter', async () => { +// openaiShim test extraction seam 036 start: parent signal abort still reaches OpenAI SSE converter +test('parent signal abort still reaches OpenAI SSE converter', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), ) + const parent = new AbortController() globalThis.fetch = (async () => stalled.response) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages - .create({ - model: 'fake-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) + .create( + { + model: 'fake-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }, + { signal: parent.signal }, + ) .withResponse() const stream = result.data as unknown as ShimStream try { const outcome = await expectAbortStopsStream({ - abort: () => stream.controller.abort(), + abort: () => parent.abort(), cancelReasons: stalled.cancelReasons, expectedEventsBeforeAbort: 3, - label: 'generic OpenAI SSE stream', + label: 'parent-aborted OpenAI SSE stream', stream, }) @@ -2476,53 +1840,58 @@ test('controller abort reaches generic OpenAI SSE converter', async () => { stalled.close() } }) -// openaiShim test extraction seam 029 end +// openaiShim test extraction seam 036 end -// openaiShim test extraction seam 030 start: controller abort cancels generic OpenAI SSE before iteration starts -test('controller abort cancels generic OpenAI SSE before iteration starts', async () => { +// openaiShim test extraction seam 037 start: parent signal abort cancels OpenAI SSE before iteration starts +test('parent signal abort cancels OpenAI SSE before iteration starts', async () => { const stalled = makeStallingResponse( makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), ) + const parent = new AbortController() globalThis.fetch = (async () => stalled.response) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages - .create({ - model: 'fake-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) + .create( + { + model: 'fake-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }, + { signal: parent.signal }, + ) .withResponse() - const stream = result.data as unknown as ShimStream + expect(result.data).toBeDefined() try { - stream.controller.abort() + parent.abort() await waitForPromise( (async () => { for (let i = 0; i < 10; i++) { if (stalled.cancelReasons.length > 0) return await Promise.resolve() } - throw new Error('pre-iteration OpenAI SSE stream did not cancel source') + throw new Error('pre-iteration parent-aborted OpenAI SSE stream did not cancel source') })(), 500, - 'pre-iteration OpenAI SSE stream did not cancel source', + 'pre-iteration parent-aborted OpenAI SSE stream did not cancel source', ) expect(stalled.cancelReasons).toHaveLength(1) } finally { stalled.close() } }) -// openaiShim test extraction seam 030 end +// openaiShim test extraction seam 037 end -// openaiShim test extraction seam 031 start: controller abort cancels generic OpenAI SSE when paused after message_start -test('controller abort cancels generic OpenAI SSE when paused after message_start', async () => { +// openaiShim test extraction seam 038 start: controller abort reaches Codex responses stream converter +test('controller abort reaches Codex responses stream converter', async () => { const stalled = makeStallingResponse( - makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), + `event: response.output_text.delta\ndata: ${JSON.stringify({ delta: 'partial' })}\n\n`, + 'https://api.example.test/v1/responses', ) globalThis.fetch = (async () => stalled.response) as unknown as FetchType @@ -2530,7 +1899,7 @@ test('controller abort cancels generic OpenAI SSE when paused after message_star const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages .create({ - model: 'fake-model', + model: 'gpt-5.4', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, @@ -2539,23 +1908,27 @@ test('controller abort cancels generic OpenAI SSE when paused after message_star const stream = result.data as unknown as ShimStream try { - await expectPausedAbortCancelsStream({ + const outcome = await expectAbortStopsStream({ + abort: () => stream.controller.abort(), cancelReasons: stalled.cancelReasons, - label: 'paused generic OpenAI SSE stream', + expectedEventsBeforeAbort: 3, + label: 'Codex responses stream', stream, }) + + expect(outcome.events.some(event => event.type === 'content_block_delta')).toBe(true) } finally { stalled.close() } }) -// openaiShim test extraction seam 031 end +// openaiShim test extraction seam 038 end -// openaiShim test extraction seam 032 start: controller abort stops buffered generic OpenAI SSE events -test('controller abort stops buffered generic OpenAI SSE events', async () => { +// openaiShim test extraction seam 039 start: controller abort cancels Codex responses stream when paused after message_start +test('controller abort cancels Codex responses stream when paused after message_start', async () => { const stalled = makeStallingResponse( - makeOpenAIStreamFrame({ role: 'assistant', content: 'first' }) + - makeOpenAIStreamFrame({ content: 'second' }), + `event: response.output_text.delta\ndata: ${JSON.stringify({ delta: 'partial' })}\n\n`, + 'https://api.example.test/v1/responses', ) globalThis.fetch = (async () => stalled.response) as unknown as FetchType @@ -2563,7 +1936,7 @@ test('controller abort stops buffered generic OpenAI SSE events', async () => { const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages .create({ - model: 'fake-model', + model: 'gpt-5.4', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, @@ -2572,36 +1945,32 @@ test('controller abort stops buffered generic OpenAI SSE events', async () => { const stream = result.data as unknown as ShimStream try { - await expectBufferedAbortRejectsNext({ - expectedText: 'first', - label: 'buffered generic OpenAI SSE stream', + await expectPausedAbortCancelsStream({ + cancelReasons: stalled.cancelReasons, + label: 'paused Codex responses stream', stream, }) - expect(stalled.cancelReasons).toHaveLength(1) } finally { stalled.close() } }) -// openaiShim test extraction seam 032 end +// openaiShim test extraction seam 039 end -// openaiShim test extraction seam 033 start: controller abort reaches Anthropic messages SSE passthrough -test('controller abort reaches Anthropic messages SSE passthrough', async () => { +// openaiShim test extraction seam 040 start: controller abort reaches Gemini SSE converter +test('controller abort reaches Gemini SSE converter', async () => { const stalled = makeStallingResponse( `data: ${JSON.stringify({ - type: 'message_start', - message: { - id: 'msg_passthrough_abort', - type: 'message', - role: 'assistant', - content: [], - model: 'passthrough-model', - stop_reason: null, - stop_sequence: null, - usage: { input_tokens: 0, output_tokens: 0 }, - }, + candidates: [ + { + content: { + role: 'model', + parts: [{ text: 'partial' }], + }, + }, + ], })}\n\n`, - 'https://api.anthropic-shaped.example.com/v1/messages', + 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse', ) globalThis.fetch = (async () => stalled.response) as unknown as FetchType @@ -2609,7 +1978,7 @@ test('controller abort reaches Anthropic messages SSE passthrough', async () => const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages .create({ - model: 'passthrough-model', + model: 'google/gemini-3.1-pro-preview', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, @@ -2621,36 +1990,35 @@ test('controller abort reaches Anthropic messages SSE passthrough', async () => const outcome = await expectAbortStopsStream({ abort: () => stream.controller.abort(), cancelReasons: stalled.cancelReasons, - expectedEventsBeforeAbort: 1, - label: 'Anthropic messages passthrough stream', + expectedEventsBeforeAbort: 3, + label: 'Gemini SSE stream', stream, }) - expect(outcome.events[0]?.type).toBe('message_start') + expect(outcome.events.some(event => event.type === 'content_block_delta')).toBe(true) } finally { stalled.close() } }) -// openaiShim test extraction seam 033 end +// openaiShim test extraction seam 040 end -// openaiShim test extraction seam 034 start: controller abort cancels Anthropic messages SSE when paused after event -test('controller abort cancels Anthropic messages SSE when paused after event', async () => { - const stalled = makeStallingResponse( +// openaiShim test extraction seam 041 start: controller abort stops buffered Gemini SSE events +test('controller abort stops buffered Gemini SSE events', async () => { + const makeGeminiFrame = (text: string) => `data: ${JSON.stringify({ - type: 'message_start', - message: { - id: 'msg_paused_passthrough_abort', - type: 'message', - role: 'assistant', - content: [], - model: 'passthrough-model', - stop_reason: null, - stop_sequence: null, - usage: { input_tokens: 0, output_tokens: 0 }, - }, - })}\n\n`, - 'https://api.anthropic-shaped.example.com/v1/messages', + candidates: [ + { + content: { + role: 'model', + parts: [{ text }], + }, + }, + ], + })}\n\n` + const stalled = makeStallingResponse( + makeGeminiFrame('first') + makeGeminiFrame('second'), + 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse', ) globalThis.fetch = (async () => stalled.response) as unknown as FetchType @@ -2658,7 +2026,7 @@ test('controller abort cancels Anthropic messages SSE when paused after event', const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages .create({ - model: 'passthrough-model', + model: 'google/gemini-3.1-pro-preview', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, @@ -2667,530 +2035,313 @@ test('controller abort cancels Anthropic messages SSE when paused after event', const stream = result.data as unknown as ShimStream try { - await expectPausedAbortCancelsStream({ - cancelReasons: stalled.cancelReasons, - label: 'paused Anthropic messages passthrough stream', + await expectBufferedAbortRejectsNext({ + expectedText: 'first', + label: 'buffered Gemini SSE stream', stream, }) + expect(stalled.cancelReasons).toHaveLength(1) } finally { stalled.close() } }) -// openaiShim test extraction seam 034 end - +// openaiShim test extraction seam 041 end -// openaiShim test extraction seam 035 start: controller abort stops buffered Anthropic messages SSE events -test('controller abort stops buffered Anthropic messages SSE events', async () => { - const stalled = makeStallingResponse( - [ - `data: ${JSON.stringify({ - type: 'message_start', - message: { - id: 'msg_buffered_passthrough_abort', - type: 'message', - role: 'assistant', - content: [], - model: 'passthrough-model', - stop_reason: null, - stop_sequence: null, - usage: { input_tokens: 0, output_tokens: 0 }, - }, - })}`, - '', - `data: ${JSON.stringify({ - type: 'content_block_start', - index: 0, - content_block: { type: 'text', text: '' }, - })}`, - '', - '', - ].join('\n'), - 'https://api.anthropic-shaped.example.com/v1/messages', - ) - globalThis.fetch = (async () => stalled.response) as unknown as FetchType +// Extraction seam: Gemini stream conversion | native Ollama stream adaptation. - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'passthrough-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const stream = result.data as unknown as ShimStream - const iterator = stream[Symbol.asyncIterator]() +// openaiShim test extraction seam 042 start: controller abort reaches native Ollama converted stream +test('controller abort reaches native Ollama converted stream', async () => { + const previousBaseUrl = process.env.OPENAI_BASE_URL + let stalled: StallingResponse | undefined try { - const first = await waitForPromise( - iterator.next(), - 500, - 'buffered Anthropic messages passthrough did not produce first event', - ) - expect(first.done).toBe(false) - expect(first.value?.type).toBe('message_start') - - stream.controller.abort() - const afterAbort = await waitForPromise( - iterator.next().then( - value => ({ status: 'resolved' as const, value }), - error => ({ status: 'rejected' as const, error }), - ), - 500, - 'buffered Anthropic messages passthrough did not stop after abort', + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' + stalled = makeStallingResponse( + `${JSON.stringify({ + model: 'llama3.1:8b', + message: { role: 'assistant', content: 'partial' }, + done: false, + })}\n`, + 'http://localhost:11434/api/chat', + 'application/x-ndjson', ) + const activeStalled = stalled - if (afterAbort.status !== 'rejected') { - throw new Error(`buffered Anthropic messages passthrough yielded after abort: ${JSON.stringify(afterAbort.value)}`) - } - expect((afterAbort.error as { name?: unknown }).name).toBe('AbortError') - expect(stalled.cancelReasons).toHaveLength(1) - } finally { - await Promise.resolve(iterator.return?.()).catch(() => {}) - stalled.close() - } -}) -// openaiShim test extraction seam 035 end - - -// openaiShim test extraction seam 036 start: parent signal abort still reaches OpenAI SSE converter -test('parent signal abort still reaches OpenAI SSE converter', async () => { - const stalled = makeStallingResponse( - makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), - ) - const parent = new AbortController() - - globalThis.fetch = (async () => stalled.response) as unknown as FetchType + globalThis.fetch = (async () => activeStalled.response) as unknown as FetchType - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create( - { - model: 'fake-model', + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'llama3.1:8b', messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: true, - }, - { signal: parent.signal }, - ) - .withResponse() - const stream = result.data as unknown as ShimStream + }) + .withResponse() + const stream = result.data as unknown as ShimStream - try { const outcome = await expectAbortStopsStream({ - abort: () => parent.abort(), - cancelReasons: stalled.cancelReasons, - expectedEventsBeforeAbort: 3, - label: 'parent-aborted OpenAI SSE stream', + abort: () => stream.controller.abort(), + cancelReasons: activeStalled.cancelReasons, + expectedEventsBeforeAbort: 1, + label: 'native Ollama converted stream', stream, }) - expect(outcome.events.some(event => event.type === 'content_block_delta')).toBe(true) + expect(outcome.events[0]?.type).toBe('message_start') } finally { - stalled.close() + stalled?.close() + restoreEnv('OPENAI_BASE_URL', previousBaseUrl) } }) -// openaiShim test extraction seam 036 end - - -// openaiShim test extraction seam 037 start: parent signal abort cancels OpenAI SSE before iteration starts -test('parent signal abort cancels OpenAI SSE before iteration starts', async () => { - const stalled = makeStallingResponse( - makeOpenAIStreamFrame({ role: 'assistant', content: 'partial' }), - ) - const parent = new AbortController() +// openaiShim test extraction seam 042 end - globalThis.fetch = (async () => stalled.response) as unknown as FetchType - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create( +// openaiShim test extraction seam 043 start: normal OpenAI SSE stream still completes after controller wiring +test('normal OpenAI SSE stream still completes after controller wiring', async () => { + globalThis.fetch = (async () => + makeSseResponse(makeStreamChunks([ { + id: 'chatcmpl-normal-stream', + object: 'chat.completion.chunk', model: 'fake-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, + choices: [ + { + index: 0, + delta: { role: 'assistant', content: 'complete' }, + finish_reason: null, + }, + ], }, - { signal: parent.signal }, - ) + { + id: 'chatcmpl-normal-stream', + object: 'chat.completion.chunk', + model: 'fake-model', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'stop', + }, + ], + }, + ]))) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'fake-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) .withResponse() - expect(result.data).toBeDefined() - try { - parent.abort() - await waitForPromise( - (async () => { - for (let i = 0; i < 10; i++) { - if (stalled.cancelReasons.length > 0) return - await Promise.resolve() - } - throw new Error('pre-iteration parent-aborted OpenAI SSE stream did not cancel source') - })(), - 500, - 'pre-iteration parent-aborted OpenAI SSE stream did not cancel source', - ) - expect(stalled.cancelReasons).toHaveLength(1) - } finally { - stalled.close() + const textDeltas: string[] = [] + for await (const event of result.data) { + const delta = (event as { delta?: { type?: string; text?: string } }).delta + if (delta?.type === 'text_delta' && typeof delta.text === 'string') { + textDeltas.push(delta.text) + } } + + expect(textDeltas.join('')).toBe('complete') + expect((result.data as unknown as ShimStream).controller.signal.aborted).toBe(false) }) -// openaiShim test extraction seam 037 end +// openaiShim test extraction seam 043 end -// openaiShim test extraction seam 038 start: controller abort reaches Codex responses stream converter -test('controller abort reaches Codex responses stream converter', async () => { - const stalled = makeStallingResponse( - `event: response.output_text.delta\ndata: ${JSON.stringify({ delta: 'partial' })}\n\n`, - 'https://api.example.test/v1/responses', - ) +// openaiShim test extraction seam 044 start: uses max_tokens instead of max_completion_tokens for local providers +test('uses max_tokens instead of max_completion_tokens for local providers', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - globalThis.fetch = (async () => stalled.response) as unknown as FetchType + globalThis.fetch = (async (_input, init) => { + const body = JSON.parse(String(init?.body)) + expect(body.options?.num_predict).toBe(64) + expect(body.options?.num_ctx).toBe(32768) + expect(body.stream_options).toBeUndefined() - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'gpt-5.4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const stream = result.data as unknown as ShimStream + return new Response( + JSON.stringify({ + model: 'llama3.1:8b', + message: { + role: 'assistant', + content: 'hello', + }, + done: true, + done_reason: 'stop', + prompt_eval_count: 5, + eval_count: 1, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType - try { - const outcome = await expectAbortStopsStream({ - abort: () => stream.controller.abort(), - cancelReasons: stalled.cancelReasons, - expectedEventsBeforeAbort: 3, - label: 'Codex responses stream', - stream, - }) + const client = createOpenAIShimClient({}) as OpenAIShimClient - expect(outcome.events.some(event => event.type === 'content_block_delta')).toBe(true) - } finally { - stalled.close() - } + await client.beta.messages.create({ + model: 'llama3.1:8b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) }) -// openaiShim test extraction seam 038 end - +// openaiShim test extraction seam 052 end -// openaiShim test extraction seam 039 start: controller abort cancels Codex responses stream when paused after message_start -test('controller abort cancels Codex responses stream when paused after message_start', async () => { - const stalled = makeStallingResponse( - `event: response.output_text.delta\ndata: ${JSON.stringify({ delta: 'partial' })}\n\n`, - 'https://api.example.test/v1/responses', - ) - globalThis.fetch = (async () => stalled.response) as unknown as FetchType +// openaiShim test extraction seam 053 start: preserves Grep tool pattern field in OpenAI-compatible schemas - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'gpt-5.4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const stream = result.data as unknown as ShimStream +// openaiShim test extraction seam 053 end - try { - await expectPausedAbortCancelsStream({ - cancelReasons: stalled.cancelReasons, - label: 'paused Codex responses stream', - stream, - }) - } finally { - stalled.close() - } -}) -// openaiShim test extraction seam 039 end +// openaiShim test extraction seam 054 start: does not infer Gemini mode from OPENAI_BASE_URL path substrings -// openaiShim test extraction seam 040 start: controller abort reaches Gemini SSE converter -test('controller abort reaches Gemini SSE converter', async () => { - const stalled = makeStallingResponse( - `data: ${JSON.stringify({ - candidates: [ - { - content: { - role: 'model', - parts: [{ text: 'partial' }], - }, - }, - ], - })}\n\n`, - 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse', - ) +// openaiShim test extraction seam 054 end - globalThis.fetch = (async () => stalled.response) as unknown as FetchType +// openaiShim test extraction seam 055 start: the OpenAI shim façade exposes the beta.messages namespace +test('the OpenAI shim façade exposes the beta.messages namespace', () => { const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const stream = result.data as unknown as ShimStream + expect(client.beta.messages).toBeDefined() +}) +// openaiShim test extraction seam 055 end - try { - const outcome = await expectAbortStopsStream({ - abort: () => stream.controller.abort(), - cancelReasons: stalled.cancelReasons, - expectedEventsBeforeAbort: 3, - label: 'Gemini SSE stream', - stream, - }) - expect(outcome.events.some(event => event.type === 'content_block_delta')).toBe(true) - } finally { - stalled.close() - } -}) -// openaiShim test extraction seam 040 end +// openaiShim test extraction seam 056 start: preserves image tool results as placeholders in follow-up requests +test('preserves image tool results as placeholders in follow-up requests', async () => { + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) -// openaiShim test extraction seam 041 start: controller abort stops buffered Gemini SSE events -test('controller abort stops buffered Gemini SSE events', async () => { - const makeGeminiFrame = (text: string) => - `data: ${JSON.stringify({ - candidates: [ - { - content: { - role: 'model', - parts: [{ text }], + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'qwen/qwen3.6-plus', + choices: [ + { + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', }, + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, }, - ], - })}\n\n` - const stalled = makeStallingResponse( - makeGeminiFrame('first') + makeGeminiFrame('second'), - 'https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse', - ) - - globalThis.fetch = (async () => stalled.response) as unknown as FetchType + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const stream = result.data as unknown as ShimStream - - try { - await expectBufferedAbortRejectsNext({ - expectedText: 'first', - label: 'buffered Gemini SSE stream', - stream, - }) - expect(stalled.cancelReasons).toHaveLength(1) - } finally { - stalled.close() - } -}) -// openaiShim test extraction seam 041 end - - -// Extraction seam: Gemini stream conversion | native Ollama stream adaptation. - -// openaiShim test extraction seam 042 start: controller abort reaches native Ollama converted stream -test('controller abort reaches native Ollama converted stream', async () => { - const previousBaseUrl = process.env.OPENAI_BASE_URL - let stalled: StallingResponse | undefined - - try { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - stalled = makeStallingResponse( - `${JSON.stringify({ - model: 'llama3.1:8b', - message: { role: 'assistant', content: 'partial' }, - done: false, - })}\n`, - 'http://localhost:11434/api/chat', - 'application/x-ndjson', - ) - const activeStalled = stalled - globalThis.fetch = (async () => activeStalled.response) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'llama3.1:8b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - const stream = result.data as unknown as ShimStream - - const outcome = await expectAbortStopsStream({ - abort: () => stream.controller.abort(), - cancelReasons: activeStalled.cancelReasons, - expectedEventsBeforeAbort: 1, - label: 'native Ollama converted stream', - stream, - }) - - expect(outcome.events[0]?.type).toBe('message_start') - } finally { - stalled?.close() - restoreEnv('OPENAI_BASE_URL', previousBaseUrl) - } -}) -// openaiShim test extraction seam 042 end - - -// openaiShim test extraction seam 043 start: normal OpenAI SSE stream still completes after controller wiring -test('normal OpenAI SSE stream still completes after controller wiring', async () => { - globalThis.fetch = (async () => - makeSseResponse(makeStreamChunks([ + await client.beta.messages.create({ + model: 'qwen/qwen3.6-plus', + system: 'test system', + messages: [ + { role: 'user', content: 'Read this screenshot' }, { - id: 'chatcmpl-normal-stream', - object: 'chat.completion.chunk', - model: 'fake-model', - choices: [ + role: 'assistant', + content: [ { - index: 0, - delta: { role: 'assistant', content: 'complete' }, - finish_reason: null, + type: 'tool_use', + id: 'call_image_1', + name: 'Read', + input: { file_path: 'C:\\temp\\screenshot.png' }, }, ], }, { - id: 'chatcmpl-normal-stream', - object: 'chat.completion.chunk', - model: 'fake-model', - choices: [ + role: 'user', + content: [ { - index: 0, - delta: {}, - finish_reason: 'stop', + type: 'tool_result', + tool_use_id: 'call_image_1', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: 'ZmFrZQ==', + }, + }, + ], }, ], }, - ]))) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'fake-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const textDeltas: string[] = [] - for await (const event of result.data) { - const delta = (event as { delta?: { type?: string; text?: string } }).delta - if (delta?.type === 'text_delta' && typeof delta.text === 'string') { - textDeltas.push(delta.text) - } - } - - expect(textDeltas.join('')).toBe('complete') - expect((result.data as unknown as ShimStream).controller.signal.aborted).toBe(false) -}) -// openaiShim test extraction seam 043 end - - -// openaiShim test extraction seam 044 start: uses max_tokens instead of max_completion_tokens for local providers -test('uses max_tokens instead of max_completion_tokens for local providers', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - - globalThis.fetch = (async (_input, init) => { - const body = JSON.parse(String(init?.body)) - expect(body.options?.num_predict).toBe(64) - expect(body.options?.num_ctx).toBe(32768) - expect(body.stream_options).toBeUndefined() - - return new Response( - JSON.stringify({ - model: 'llama3.1:8b', - message: { - role: 'assistant', - content: 'hello', - }, - done: true, - done_reason: 'stop', - prompt_eval_count: 5, - eval_count: 1, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'llama3.1:8b', - messages: [{ role: 'user', content: 'hello' }], + ], max_tokens: 64, stream: false, }) -}) -// openaiShim test extraction seam 044 end - -test('does not send stream_options to local OpenAI-compatible servers', async () => { - process.env.OPENAI_BASE_URL = 'http://127.0.0.1:8000/v1' - globalThis.fetch = (async (_input, init) => { - const body = JSON.parse(String(init?.body)) - expect(body.stream).toBe(true) - expect(body.stream_options).toBeUndefined() - return new Response('', { - headers: { 'Content-Type': 'text/event-stream' }, - }) - }) as unknown as FetchType + const toolMessage = (requestBody?.messages as Array>).find( + message => message.role === 'tool', + ) as { + content?: Array<{ + type: string + text?: string + image_url?: { url: string } + }> | string + } | undefined - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'local-vllm-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) + expect(Array.isArray(toolMessage?.content)).toBe(true) + const parts = toolMessage?.content as Array<{ + type: string + text?: string + image_url?: { url: string } + }> + // Issue #1421: image-only tool results now get a placeholder text part + // prepended so OpenAI-compatible providers that require a `text` field on + // `role: "tool"` messages (e.g. Xiaomi Mimo) don't 400 with "text is not set". + expect(parts).toEqual([ + { type: 'text', text: 'Image attached.' }, + { + type: 'image_url', + image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, + }, + ]) }) +// openaiShim test extraction seam 056 end -// openaiShim test extraction seam 045 start: keeps max_completion_tokens for non-local non-github providers -test('keeps max_completion_tokens for non-local non-github providers', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + +// openaiShim test extraction seam 057 start: adds text part for image-only user messages +test('adds text part for image-only user messages', async () => { + let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { - const body = JSON.parse(String(init?.body)) - expect(body.max_completion_tokens).toBe(64) - expect(body.max_tokens).toBeUndefined() + requestBody = JSON.parse(String(init?.body)) return new Response( JSON.stringify({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'mimo-v2.5-pro', choices: [ { message: { role: 'assistant', - content: 'hello', + content: 'done', }, finish_reason: 'stop', }, ], usage: { - prompt_tokens: 5, - completion_tokens: 1, - total_tokens: 6, + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, }, }), { @@ -3204,44 +2355,72 @@ test('keeps max_completion_tokens for non-local non-github providers', async () const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'gpt-4o', - messages: [{ role: 'user', content: 'hello' }], + model: 'mimo-v2.5-pro', + system: 'test system', + messages: [ + { + role: 'user', + content: [ + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: 'ZmFrZQ==', + }, + }, + ], + }, + ], max_tokens: 64, stream: false, }) -}) -// openaiShim test extraction seam 045 end + const userMessage = (requestBody?.messages as Array>).find( + message => message.role === 'user', + ) as { + content?: Array<{ + type: string + text?: string + image_url?: { url: string } + }> + } | undefined + + expect(userMessage?.content).toEqual([ + { type: 'text', text: 'Image attached.' }, + { + type: 'image_url', + image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, + }, + ]) +}) +// openaiShim test extraction seam 057 end -// openaiShim test extraction seam 046 start: uses route-specific credential env vars for descriptor-backed openai-compatible routes -test('uses route-specific credential env vars for descriptor-backed openai-compatible routes', async () => { - let capturedHeaders: Headers | undefined - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' - process.env.OPENROUTER_API_KEY = 'or-route-key' - delete process.env.OPENAI_API_KEY +// openaiShim test extraction seam 058 start: preserves mixed text and image tool results as multipart content +test('preserves mixed text and image tool results as multipart content', async () => { + let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { - capturedHeaders = new Headers(init?.headers) + requestBody = JSON.parse(String(init?.body)) return new Response( JSON.stringify({ id: 'chatcmpl-1', - model: 'openai/gpt-5-mini', + model: 'gpt-4o', choices: [ { message: { role: 'assistant', - content: 'ok', + content: 'done', }, finish_reason: 'stop', }, ], usage: { - prompt_tokens: 5, - completion_tokens: 1, - total_tokens: 6, + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, }, }), { @@ -3255,80 +2434,18 @@ test('uses route-specific credential env vars for descriptor-backed openai-compa const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'openai/gpt-5-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(capturedHeaders?.get('authorization')).toBe('Bearer or-route-key') -}) -// openaiShim test extraction seam 046 end - - -// openaiShim test extraction seam 047 start: preserves Gemini tool call extra_content in follow-up requests - -// openaiShim test extraction seam 047 end - - -// openaiShim test extraction seam 048 start: replays Gemini tool signatures for OpenGateway Gemini models - -// openaiShim test extraction seam 048 end - - -// openaiShim test extraction seam 049 start: OpenGateway MiMo replays real reasoning_content without adding empty fallback -test('OpenGateway MiMo replays real reasoning_content without adding empty fallback', async () => { - process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-opengateway-mimo', - model: 'mimo-v2.5-pro', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', + model: 'gpt-4o', + system: 'test system', messages: [ - { role: 'user', content: 'Use an agent' }, + { role: 'user', content: 'Read this screenshot' }, { role: 'assistant', content: [ - { - type: 'thinking', - thinking: 'Need to inspect code with an agent.', - }, { type: 'tool_use', - id: 'call_agent_1', - name: 'Agent', - input: { - description: 'Inspect code', - prompt: 'Look at the relevant code', - subagent_type: 'general-purpose', - }, + id: 'call_image_2', + name: 'Read', + input: { file_path: 'C:\\temp\\screenshot.png' }, }, ], }, @@ -3337,8 +2454,18 @@ test('OpenGateway MiMo replays real reasoning_content without adding empty fallb content: [ { type: 'tool_result', - tool_use_id: 'call_agent_1', - content: 'Agent finished', + tool_use_id: 'call_image_2', + content: [ + { type: 'text', text: 'Screenshot captured' }, + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: 'ZmFrZQ==', + }, + }, + ], }, ], }, @@ -3347,1782 +2474,1449 @@ test('OpenGateway MiMo replays real reasoning_content without adding empty fallb stream: false, }) - const assistantWithToolCall = (requestBody?.messages as Array>).find( - message => Array.isArray(message.tool_calls), - ) + const toolMessage = (requestBody?.messages as Array>).find( + message => message.role === 'tool', + ) as { + content?: Array<{ + type: string + text?: string + image_url?: { url: string } + }> + } | undefined - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to inspect code with an agent.', - ) - expect(requestBody).not.toHaveProperty('store') + expect(Array.isArray(toolMessage?.content)).toBe(true) + const parts = toolMessage?.content ?? [] + expect(parts[0]).toEqual({ type: 'text', text: 'Screenshot captured' }) + expect(parts[1]).toEqual({ + type: 'image_url', + image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, + }) }) -// openaiShim test extraction seam 049 end - +// openaiShim test extraction seam 074 end -// openaiShim test extraction seam 050 start: Xiaomi MiMo replays real reasoning_content without adding empty fallback -test('Xiaomi MiMo replays real reasoning_content without adding empty fallback', async () => { - process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - process.env.MIMO_API_KEY = 'mimo-test-key' +test('longcat provider flag prefers LONGCAT_API_KEY over generic OPENAI_API_KEYS pool', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' + delete process.env.OPENAI_BASE_URL delete process.env.OPENAI_API_KEY - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) + const result = applyProviderFlag('longcat', []) + expect(result.error).toBeUndefined() - return new Response( - JSON.stringify({ - id: 'chatcmpl-mimo', - model: 'mimo-v2.5-pro', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-longcat-key') +}) + +test('longcat provider flag strips unsupported tool definitions', async () => { + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) as Record + return makeChatCompletionResponse('LongCat-2.0') }) as unknown as FetchType - const client = createOpenAIShimClient({}) as OpenAIShimClient + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_KEY + + expect(applyProviderFlag('longcat', []).error).toBeUndefined() + const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'mimo-v2.5-pro', + model: 'LongCat-2.0', messages: [ - { role: 'user', content: 'Use an agent' }, - { - role: 'assistant', - content: [ - { - type: 'thinking', - thinking: 'Need to inspect code with an agent.', - }, - { - type: 'tool_use', - id: 'call_agent_1', - name: 'Agent', - input: { - description: 'Inspect code', - prompt: 'Look at the relevant code', - subagent_type: 'general-purpose', - }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_agent_1', - content: 'Agent finished', - }, - ], - }, + { role: 'user', content: 'List files' }, ], - max_tokens: 64, + tools: [{ + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }], + max_tokens: 32, stream: false, }) - const assistantWithToolCall = (requestBody?.messages as Array>).find( - message => Array.isArray(message.tool_calls), - ) - - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to inspect code with an agent.', - ) - expect(requestBody).not.toHaveProperty('store') + expect(requestBody?.tools).toBeUndefined() }) -// openaiShim test extraction seam 050 end +test('longcat rejects image input before dispatch', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_KEY -// openaiShim test extraction seam 051 start: OpenGateway MiMo does not synthesize empty reasoning_content when missing -test('OpenGateway MiMo does not synthesize empty reasoning_content when missing', async () => { - process.env.OPENAI_BASE_URL = 'https://opengateway.gitlawb.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - let requestBody: Record | undefined + expect(applyProviderFlag('longcat', []).error).toBeUndefined() - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect(client.beta.messages.create({ + model: 'LongCat-2.0', + messages: [{ + role: 'user', + content: [ + { type: 'text', text: 'Describe this image.' }, + { type: 'image', source: { type: 'url', url: 'https://example.com/image.png' } }, + ], + }], + max_tokens: 32, + stream: false, + })).rejects.toThrow('does not support image inputs') +}) - return new Response( - JSON.stringify({ - id: 'chatcmpl-opengateway-mimo', - model: 'mimo-v2.5-pro', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType +test('longcat rejects image tool results before dispatch', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_KEY + expect(applyProviderFlag('longcat', []).error).toBeUndefined() const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', + await expect(client.beta.messages.create({ + model: 'LongCat-2.0', messages: [ - { role: 'user', content: 'Use an agent' }, - { - role: 'assistant', - content: [ - { - type: 'tool_use', - id: 'call_agent_1', - name: 'Agent', - input: { - description: 'Inspect code', - prompt: 'Look at the relevant code', - subagent_type: 'general-purpose', - }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_agent_1', - content: 'Agent finished', - }, - ], - }, + { role: 'user', content: 'Inspect the screenshot' }, + { role: 'assistant', content: [{ type: 'tool_use', id: 'call_screenshot', name: 'Screenshot', input: {} }] }, + { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_screenshot', content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'aGVsbG8=' } }] }] }, ], - max_tokens: 64, + tools: [{ name: 'Screenshot', description: 'Capture a screenshot', input_schema: { type: 'object', properties: {} } }], + max_tokens: 32, stream: false, - }) + })).rejects.toThrow('does not support image inputs') +}) - const assistantWithToolCall = (requestBody?.messages as Array>).find( - message => Array.isArray(message.tool_calls), - ) +test('longcat accepts the documented bare OpenAI SDK base URL', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai' + delete process.env.OPENAI_API_KEY - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall).not.toHaveProperty('reasoning_content') - expect(requestBody).not.toHaveProperty('store') -}) -// openaiShim test extraction seam 051 end + expect(applyProviderFlag('longcat', []).error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-longcat-key') +}) -// openaiShim test extraction seam 052 start: strips unsupported stream_options for Xiaomi MiMo streams -test('strips unsupported stream_options for Xiaomi MiMo streams', async () => { - process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - process.env.MIMO_API_KEY = 'mimo-test-key' +test('longcat accepts the documented bare OpenAI SDK base URL with a trailing slash', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/' delete process.env.OPENAI_API_KEY - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) + expect(applyProviderFlag('longcat', []).error).toBeUndefined() - return makeSseResponse( - makeStreamChunks([ - { - id: 'chatcmpl-mimo', - object: 'chat.completion.chunk', - model: 'mimo-v2.5-pro', - choices: [ - { - index: 0, - delta: { role: 'assistant', content: 'done' }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-mimo', - object: 'chat.completion.chunk', - model: 'mimo-v2.5-pro', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'stop', - }, - ], - }, - ]), - ) - }) as unknown as FetchType + const captured = await captureChatCompletionRequest() - const client = createOpenAIShimClient({}) as OpenAIShimClient + expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-longcat-key') +}) - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) +test('longcat does not append chat completions to a configured endpoint URL', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/v1/chat/completions' + delete process.env.OPENAI_API_KEY - expect(requestBody).toMatchObject({ - stream: true, - max_completion_tokens: 64, - }) - expect(requestBody).not.toHaveProperty('stream_options') - expect(requestBody).not.toHaveProperty('store') + expect(applyProviderFlag('longcat', []).error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') + expect(captured.authorization).toBe('Bearer fake-longcat-key') }) -// openaiShim test extraction seam 052 end +test('longcat normalizes a configured endpoint URL with a trailing slash', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/v1/chat/completions/' + delete process.env.OPENAI_API_KEY -// openaiShim test extraction seam 053 start: preserves Grep tool pattern field in OpenAI-compatible schemas + expect(applyProviderFlag('longcat', []).error).toBeUndefined() + const captured = await captureChatCompletionRequest() + expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') +}) -// openaiShim test extraction seam 053 end +test('longcat accepts the documented CodeBuddy endpoint URL', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/chat/completions' + delete process.env.OPENAI_API_KEY + expect(applyProviderFlag('longcat', []).error).toBeUndefined() + const captured = await captureChatCompletionRequest() + expect(captured.url).toBe('https://api.longcat.chat/openai/chat/completions') +}) -// openaiShim test extraction seam 054 start: does not infer Gemini mode from OPENAI_BASE_URL path substrings +test('longcat prefers its dedicated credential over a copied key from another provider', async () => { + process.env.LONGCAT_API_KEY = 'fake-longcat-key' + process.env.MIMO_API_KEY = 'fake-mimo-key' + process.env.OPENAI_API_KEY = 'fake-mimo-key' + process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/v1' -// openaiShim test extraction seam 054 end + const captured = await captureChatCompletionRequest('LongCat-2.0') + expect(captured.authorization).toBe('Bearer fake-longcat-key') +}) -// openaiShim test extraction seam 055 start: the OpenAI shim façade exposes the beta.messages namespace -test('the OpenAI shim façade exposes the beta.messages namespace', () => { - const client = createOpenAIShimClient({}) as OpenAIShimClient - expect(client.beta.messages).toBeDefined() +test('longcat provider flag never falls back to an OPENAI_API_KEYS pool', async () => { + process.env.OPENAI_API_KEYS = 'other-provider-secret' + delete process.env.LONGCAT_API_KEY + delete process.env.OPENAI_BASE_URL + delete process.env.OPENAI_API_KEY + + const result = applyProviderFlag('longcat', []) + expect(result.error).toBeUndefined() + + const captured = await captureChatCompletionRequest() + + expect(captured.authorization).not.toBe('Bearer other-provider-secret') }) -// openaiShim test extraction seam 055 end +test('dedicated-only ClinePass route never falls back to generic OpenAI credentials', async () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.cline.bot/api/v1' + process.env.OPENAI_MODEL = 'cline-pass/deepseek-v4-flash' + process.env.OPENAI_API_KEY = 'generic-openai-key' + process.env.OPENAI_API_KEYS = 'generic-openai-pool-a,generic-openai-pool-b' + delete process.env.CLINE_API_KEY -// openaiShim test extraction seam 056 start: preserves image tool results as placeholders in follow-up requests -test('preserves image tool results as placeholders in follow-up requests', async () => { - let requestBody: Record | undefined + const captured = await captureChatCompletionRequest( + 'cline-pass/deepseek-v4-flash', + ) - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) + expect(captured.authorization).toBeNull() +}) +// openaiShim test extraction seam 087 end - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'qwen/qwen3.6-plus', + +// openaiShim test extraction seam 088 start: preserves Gemini tool call extra_content from streaming chunks + +// openaiShim test extraction seam 088 end + + +// openaiShim test extraction seam 089 start: preserves Gemini thought signature from streaming delta extra_content + +// openaiShim test extraction seam 089 end + + +// openaiShim test extraction seam 090 start: preserves Gemini thought signature from non-streaming message extra_content + +// openaiShim test extraction seam 090 end + + +// Extraction seam: provider signature metadata | raw streaming tool fallback. + +// openaiShim test extraction seam 091 start: converts Gemini raw tool-call text into streaming tool_use blocks +test('converts Gemini raw tool-call text into streaming tool_use blocks', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-raw-tool', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-flash-lite', choices: [ { - message: { + index: 0, + delta: { role: 'assistant', - content: 'done', + content: 'Tool calls', }, - finish_reason: 'stop', + finish_reason: null, }, ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'qwen/qwen3.6-plus', - system: 'test system', - messages: [ - { role: 'user', content: 'Read this screenshot' }, { - role: 'assistant', - content: [ + id: 'chatcmpl-raw-tool', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-flash-lite', + choices: [ { - type: 'tool_use', - id: 'call_image_1', - name: 'Read', - input: { file_path: 'C:\\temp\\screenshot.png' }, + index: 0, + delta: { + content: + ' requested:\n- Write({"file_path":"style.css","content":"ul { padding: 0; }"}) [id: call79435b5a26564619b0151197]', + }, + finish_reason: null, }, ], }, { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_image_1', - content: [ - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: 'ZmFrZQ==', - }, - }, - ], - }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - - const toolMessage = (requestBody?.messages as Array>).find( - message => message.role === 'tool', - ) as { - content?: Array<{ - type: string - text?: string - image_url?: { url: string } - }> | string - } | undefined - - expect(Array.isArray(toolMessage?.content)).toBe(true) - const parts = toolMessage?.content as Array<{ - type: string - text?: string - image_url?: { url: string } - }> - // Issue #1421: image-only tool results now get a placeholder text part - // prepended so OpenAI-compatible providers that require a `text` field on - // `role: "tool"` messages (e.g. Xiaomi Mimo) don't 400 with "text is not set". - expect(parts).toEqual([ - { type: 'text', text: 'Image attached.' }, - { - type: 'image_url', - image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, - }, - ]) -}) -// openaiShim test extraction seam 056 end - - -// openaiShim test extraction seam 057 start: adds text part for image-only user messages -test('adds text part for image-only user messages', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'mimo-v2.5-pro', + id: 'chatcmpl-raw-tool', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-flash-lite', choices: [ { - message: { - role: 'assistant', - content: 'done', - }, + index: 0, + delta: {}, finish_reason: 'stop', }, ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, }, - ) + ]) + + return makeSseResponse(chunks) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', - system: 'test system', - messages: [ - { - role: 'user', - content: [ - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: 'ZmFrZQ==', - }, - }, - ], - }, - ], - max_tokens: 64, - stream: false, + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-flash-lite', + messages: [{ role: 'user', content: 'Write CSS' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } + + expect( + events.some( + event => + event.type === 'content_block_start' && + (event.content_block as Record | undefined)?.type === + 'text', + ), + ).toBe(false) + + const toolStart = events.find( + event => + event.type === 'content_block_start' && + (event.content_block as Record | undefined)?.type === + 'tool_use', + ) as { content_block?: Record } | undefined + expect(toolStart?.content_block).toMatchObject({ + type: 'tool_use', + id: 'call79435b5a26564619b0151197', + name: 'Write', }) - const userMessage = (requestBody?.messages as Array>).find( - message => message.role === 'user', - ) as { - content?: Array<{ - type: string - text?: string - image_url?: { url: string } - }> - } | undefined + const toolInput = events + .filter( + event => + event.type === 'content_block_delta' && + (event.delta as Record | undefined)?.type === + 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') + expect(JSON.parse(toolInput)).toEqual({ + file_path: 'style.css', + content: 'ul { padding: 0; }', + }) - expect(userMessage?.content).toEqual([ - { type: 'text', text: 'Image attached.' }, - { - type: 'image_url', - image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, - }, - ]) + const stop = events.find(event => event.type === 'message_delta') as + | { delta?: Record } + | undefined + expect(stop?.delta?.stop_reason).toBe('tool_use') }) -// openaiShim test extraction seam 057 end - +// openaiShim test extraction seam 091 end -// openaiShim test extraction seam 058 start: preserves mixed text and image tool results as multipart content -test('preserves mixed text and image tool results as multipart content', async () => { - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) +// Extraction seam: streaming conversion | non-streaming response conversion. - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-4o', - choices: [ - { - message: { - role: 'assistant', - content: 'done', +// openaiShim test extraction seam 092 start: converts Gemini raw tool-call text into non-streaming tool_use blocks +test('converts Gemini raw tool-call text into non-streaming tool_use blocks', async () => { + const previousFetch = globalThis.fetch + try { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-raw-tool', + model: 'google/gemini-3.1-flash-lite', + choices: [ + { + message: { + role: 'assistant', + content: + 'Tool calls requested:\n- Agent({"description":"Verify the todo list application functionality.","prompt":"Check files.","subagent_type":"verification"}) [id: call9a8b7c6d5e4f3a2b1c0d9e8f]', + }, + finish_reason: 'stop', }, - finish_reason: 'stop', + ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, }, - }), + ) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + + const message = await client.beta.messages.create({ + model: 'google/gemini-3.1-flash-lite', + messages: [{ role: 'user', content: 'Verify' }], + max_tokens: 64, + stream: false, + }) as { + stop_reason?: string + content?: Array> + } + + expect(message.stop_reason).toBe('tool_use') + expect(message.content).toEqual([ { - headers: { - 'Content-Type': 'application/json', + type: 'tool_use', + id: 'call9a8b7c6d5e4f3a2b1c0d9e8f', + name: 'Agent', + input: { + description: 'Verify the todo list application functionality.', + prompt: 'Check files.', + subagent_type: 'verification', }, }, - ) - }) as unknown as FetchType + ]) + } finally { + globalThis.fetch = previousFetch + } +}) +// openaiShim test extraction seam 095 end - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'gpt-4o', - system: 'test system', - messages: [ - { role: 'user', content: 'Read this screenshot' }, +// Extraction seam: completed tool parsing | streamed tool normalization. + +// openaiShim test extraction seam 096 start: normalizes plain string Bash tool arguments in streaming responses +test('normalizes plain string Bash tool arguments in streaming responses', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ { - role: 'assistant', - content: [ + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ { - type: 'tool_use', - id: 'call_image_2', - name: 'Read', - input: { file_path: 'C:\\temp\\screenshot.png' }, + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: 'pwd', + }, + }, + ], + }, + finish_reason: null, }, ], }, { - role: 'user', - content: [ + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ { - type: 'tool_result', - tool_use_id: 'call_image_2', - content: [ - { type: 'text', text: 'Screenshot captured' }, - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: 'ZmFrZQ==', - }, - }, - ], + index: 0, + delta: {}, + finish_reason: 'tool_calls', }, ], }, - ], - max_tokens: 64, - stream: false, - }) + ]) - const toolMessage = (requestBody?.messages as Array>).find( - message => message.role === 'tool', - ) as { - content?: Array<{ - type: string - text?: string - image_url?: { url: string } - }> - } | undefined - - expect(Array.isArray(toolMessage?.content)).toBe(true) - const parts = toolMessage?.content ?? [] - expect(parts[0]).toEqual({ type: 'text', text: 'Screenshot captured' }) - expect(parts[1]).toEqual({ - type: 'image_url', - image_url: { url: 'data:image/png;base64,ZmFrZQ==' }, - }) -}) -// openaiShim test extraction seam 058 end - - -// openaiShim test extraction seam 059 start: uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests -test('uses GEMINI_ACCESS_TOKEN for Gemini OpenAI-compatible requests', async () => { - let capturedAuthorization: string | null = null - let capturedProject: string | null = null - let requestUrl: string | undefined - - process.env.CLAUDE_CODE_USE_GEMINI = '1' - process.env.GEMINI_AUTH_MODE = 'access-token' - process.env.GEMINI_ACCESS_TOKEN = 'gemini-access-token' - process.env.GOOGLE_CLOUD_PROJECT = 'gemini-project' - process.env.GEMINI_BASE_URL = - 'https://generativelanguage.googleapis.com/v1beta/openai' - process.env.GEMINI_MODEL = 'gemini-2.0-flash' - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_API_KEY - delete process.env.GEMINI_API_KEY - delete process.env.GOOGLE_API_KEY - - globalThis.fetch = (async (input, init) => { - requestUrl = typeof input === 'string' ? input : input.url - const headers = init?.headers as Record | undefined - capturedAuthorization = - headers?.Authorization ?? headers?.authorization ?? null - capturedProject = - headers?.['x-goog-user-project'] ?? - headers?.['X-Goog-User-Project'] ?? - null - - return new Response( - JSON.stringify({ - id: 'chatcmpl-gemini', - model: 'gemini-2.0-flash', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 3, - completion_tokens: 1, - total_tokens: 4, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType + return makeSseResponse(chunks) + }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'gemini-2.0-flash', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(requestUrl).toBe( - 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', - ) - // Explicit type argument: TS narrows the closure-assigned variables to - // their `null` initializer at this point (microsoft/TypeScript#9998). - expect(capturedAuthorization).toBe('Bearer gemini-access-token') - expect(capturedProject).toBe('gemini-project') -}) -// openaiShim test extraction seam 059 end + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } -// openaiShim test extraction seam 060 start: uses NVIDIA_API_KEY for NVIDIA NIM requests without OPENAI_API_KEY -test('uses NVIDIA_API_KEY for NVIDIA NIM requests without OPENAI_API_KEY', async () => { - let capturedAuthorization: string | null = null + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.NVIDIA_NIM = '1' - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.OPENAI_MODEL = 'nvidia/llama-3.1-nemotron-70b-instruct' - process.env.NVIDIA_API_KEY = 'nvidia-live-key' - delete process.env.OPENAI_API_KEY + expect(normalizedInput).toBe('{"command":"pwd"}') +}) +// openaiShim test extraction seam 096 end - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - capturedAuthorization = - headers?.Authorization ?? headers?.authorization ?? null - return new Response( - JSON.stringify({ - id: 'chatcmpl-nvidia', - model: 'nvidia/llama-3.1-nemotron-70b-instruct', +// openaiShim test extraction seam 097 start: normalizes plain string Bash tool arguments when streaming starts with an empty chunk +test('normalizes plain string Bash tool arguments when streaming starts with an empty chunk', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', choices: [ { - message: { + index: 0, + delta: { role: 'assistant', - content: 'ok', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '', + }, + }, + ], }, - finish_reason: 'stop', + finish_reason: null, }, ], - usage: { - prompt_tokens: 3, - completion_tokens: 1, - total_tokens: 4, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'nvidia/llama-3.1-nemotron-70b-instruct', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(capturedAuthorization).toBe('Bearer nvidia-live-key') -}) -// openaiShim test extraction seam 060 end - - -// openaiShim test extraction seam 061 start: does not use stale NVIDIA_API_KEY for non-NVIDIA OpenAI-compatible routes -test('does not use stale NVIDIA_API_KEY for non-NVIDIA OpenAI-compatible routes', async () => { - let capturedAuthorization: string | null = null - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.NVIDIA_NIM = '1' - process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' - process.env.OPENAI_MODEL = 'openai/gpt-5-mini' - process.env.NVIDIA_API_KEY = 'nvidia-live-key' - delete process.env.OPENAI_API_KEY - delete process.env.OPENROUTER_API_KEY - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - capturedAuthorization = - headers?.Authorization ?? headers?.authorization ?? null - - return new Response( - JSON.stringify({ - id: 'chatcmpl-openrouter', - model: 'openai/gpt-5-mini', + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', choices: [ { - message: { - role: 'assistant', - content: 'ok', + index: 0, + delta: { + tool_calls: [ + { + index: 0, + type: 'function', + function: { + arguments: 'pwd', + }, + }, + ], }, - finish_reason: 'stop', + finish_reason: null, }, ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'openai/gpt-5-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(capturedAuthorization).toBeNull() -}) -// openaiShim test extraction seam 061 end - - -// openaiShim test extraction seam 062 start: does not use MINIMAX_API_KEY for non-MiniMax OpenAI-compatible routes -test('does not use MINIMAX_API_KEY for non-MiniMax OpenAI-compatible routes', async () => { - let capturedAuthorization: string | null = null - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' - process.env.OPENAI_MODEL = 'openai/gpt-5-mini' - process.env.MINIMAX_API_KEY = 'minimax-live-key' - delete process.env.OPENAI_API_KEY - delete process.env.OPENROUTER_API_KEY - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - capturedAuthorization = - headers?.Authorization ?? headers?.authorization ?? null - - return new Response( - JSON.stringify({ - id: 'chatcmpl-openrouter', - model: 'openai/gpt-5-mini', + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', choices: [ { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', + index: 0, + delta: {}, + finish_reason: 'tool_calls', }, ], - }), - { - headers: { - 'Content-Type': 'application/json', - }, }, - ) + ]) + + return makeSseResponse(chunks) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'openai/gpt-5-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(capturedAuthorization).toBeNull() -}) -// openaiShim test extraction seam 062 end + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } -// openaiShim test extraction seam 063 start: xiaomi mimo route uses api-key auth header and max_completion_tokens -test('xiaomi mimo route uses api-key auth header and max_completion_tokens', async () => { - let capturedHeaders: Record | undefined - let capturedBody: Record | undefined + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.xiaomimimo.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - process.env.MIMO_API_KEY = 'mimo-live-key' - delete process.env.OPENAI_API_KEY + expect(normalizedInput).toBe('{"command":"pwd"}') +}) +// openaiShim test extraction seam 097 end - globalThis.fetch = (async (_input, init) => { - capturedHeaders = init?.headers as Record - capturedBody = JSON.parse(String(init?.body)) as Record - return new Response( - JSON.stringify({ - id: 'chatcmpl-mimo', - model: 'mimo-v2.5-pro', +// openaiShim test extraction seam 098 start: normalizes plain string Bash tool arguments when streaming starts with whitespace +test('normalizes plain string Bash tool arguments when streaming starts with whitespace', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', choices: [ { - message: { + index: 0, + delta: { role: 'assistant', - content: 'ok', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: ' ', + }, + }, + ], }, - finish_reason: 'stop', + finish_reason: null, }, ], - }), + }, { - headers: { - 'Content-Type': 'application/json', - }, + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + type: 'function', + function: { + arguments: 'pwd', + }, + }, + ], + }, + finish_reason: null, + }, + ], }, - ) + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'tool_calls', + }, + ], + }, + ]) + + return makeSseResponse(chunks) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(capturedHeaders).toMatchObject({ 'api-key': 'mimo-live-key' }) - expect(capturedHeaders).not.toHaveProperty('Authorization') - expect(capturedBody).toMatchObject({ max_completion_tokens: 32 }) - expect(capturedBody).not.toHaveProperty('max_tokens') -}) -// openaiShim test extraction seam 063 end - -// openaiShim test extraction seam 064 start: xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_effort -test('xiaomi mimo token plan uses raw api-key and OpenAI-compatible reasoning_effort', async () => { - let capturedHeaders: Record | undefined - let capturedBody: Record | undefined - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://token-plan-sgp.xiaomimimo.com/v1' - process.env.OPENAI_MODEL = 'mimo-v2.5-pro' - process.env.MIMO_API_KEY = 'mimo-token-key' - delete process.env.OPENAI_API_KEY - - globalThis.fetch = (async (_input, init) => { - capturedHeaders = init?.headers as Record - capturedBody = JSON.parse(String(init?.body)) as Record - - return makeChatCompletionResponse('mimo-v2.5-pro') - }) as unknown as FetchType + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const client = createOpenAIShimClient({ - reasoningEffort: 'high', - }) as OpenAIShimClient + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - await client.beta.messages.create({ - model: 'mimo-v2.5-pro', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - expect(capturedHeaders).toMatchObject({ 'api-key': 'mimo-token-key' }) - expect(capturedHeaders).not.toHaveProperty('Authorization') - expect(capturedBody).toMatchObject({ - max_completion_tokens: 32, - reasoning_effort: 'high', - }) - expect(capturedBody).not.toHaveProperty('max_tokens') - expect(capturedBody).not.toHaveProperty('store') - expect(capturedBody).not.toHaveProperty('stream_options') + expect(normalizedInput).toBe('{"command":" pwd"}') }) -// openaiShim test extraction seam 064 end - - -test.each([ - 'minimax-m3', - 'minimax-m2.7', - 'qwen3.7-max', - 'qwen3.7-plus', - 'qwen3.6-plus', -])('opencode go %s direct env routing ignores stale custom auth and uses the Anthropic Messages request contract', async model => { - let capturedUrl = '' - let capturedHeaders: Headers | undefined - let capturedBody: Record | undefined - - process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' - delete process.env.OPENAI_API_KEY - process.env.OPENAI_MODEL = model - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENCODE_API_KEY = 'fake-opencode-key' - process.env.OPENAI_AUTH_HEADER = 'Authorization' - process.env.OPENAI_AUTH_SCHEME = 'bearer' - process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-header-value' - - globalThis.fetch = (async (input, init) => { - capturedUrl = String(input) - capturedHeaders = new Headers(init?.headers) - capturedBody = JSON.parse(String(init?.body)) as Record - - return new Response( - JSON.stringify({ - id: 'msg_opencode_go', - type: 'message', - role: 'assistant', - model, - content: [{ type: 'text', text: 'ok' }], - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 1, - output_tokens: 1, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType +// openaiShim test extraction seam 098 end - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model, - system: 'test system', - messages: [ +// openaiShim test extraction seam 099 start: keeps terminal whitespace-only Bash arguments invalid in streaming responses +test('keeps terminal whitespace-only Bash arguments invalid in streaming responses', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ { - role: 'user', - content: [{ type: 'text', text: 'hello' }], + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: ' ', + }, + }, + ], + }, + finish_reason: null, + }, + ], }, - ], - max_tokens: 32, - stream: false, - }) - - expect(capturedUrl).toBe('https://opencode.ai/zen/go/v1/messages') - expect(capturedHeaders?.get('x-api-key')).toBe('fake-opencode-key') - expect(capturedHeaders?.get('authorization')).toBeNull() - expect(capturedBody).toEqual({ - model, - messages: [ { - role: 'user', - content: [{ type: 'text', text: 'hello' }], + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'tool_calls', + }, + ], }, - ], - max_tokens: 32, - stream: false, - system: 'test system', - }) - expect(capturedBody).not.toHaveProperty('max_completion_tokens') - expect(capturedBody).not.toHaveProperty('store') -}) - -// openaiShim test extraction seam 065 start: opencode go messages endpoint rotates raw x-api-key credentials after rate-limit failure -test('opencode go messages endpoint rotates raw x-api-key credentials after rate-limit failure', async () => { - const capturedUrls: string[] = [] - const capturedKeys: Array = [] - - process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_API_KEYS - process.env.OPENAI_MODEL = 'minimax-m3' - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENCODE_API_KEY = 'fake-opencode-a,fake-opencode-b' - - globalThis.fetch = (async (input, init) => { - const headers = new Headers(init?.headers) - capturedUrls.push(String(input)) - capturedKeys.push(headers.get('x-api-key')) - - if (capturedKeys.length === 1) { - return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { - status: 429, - headers: { 'Content-Type': 'application/json' }, - }) - } + ]) - return new Response( - JSON.stringify({ - id: 'msg_opencode_go_retry', - type: 'message', - role: 'assistant', - model: 'minimax-m3', - content: [{ type: 'text', text: 'ok' }], - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 1, - output_tokens: 1, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) + return makeSseResponse(chunks) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'minimax-m3', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(capturedUrls).toEqual([ - 'https://opencode.ai/zen/go/v1/messages', - 'https://opencode.ai/zen/go/v1/messages', - ]) - expect(capturedKeys).toEqual(['fake-opencode-a', 'fake-opencode-b']) -}) -// openaiShim test extraction seam 065 end - - -// openaiShim test extraction seam 066 start: gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY as bearer auth despite stale generic base URL -test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY as bearer auth despite stale generic base URL', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' - delete process.env.OPENAI_API_KEY - - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() - - const captured = await captureChatCompletionRequest() - - expect(captured.url).toBe('https://opengateway.gitlawb.com/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-ogw-key') -}) -// openaiShim test extraction seam 066 end - - -// openaiShim test extraction seam 067 start: gitlawb opengateway provider flag accepts OPENAI_API_KEY compatibility fallback -test('gitlawb opengateway provider flag accepts OPENAI_API_KEY compatibility fallback', async () => { - delete process.env.OPENAI_BASE_URL - delete process.env.OPENGATEWAY_API_KEY - process.env.OPENAI_API_KEY = 'fake-openai-fallback' + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - const captured = await captureChatCompletionRequest() + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - expect(captured.authorization).toBe('Bearer fake-openai-fallback') + expect(normalizedInput).toBe('{}') }) -// openaiShim test extraction seam 067 end - +// openaiShim test extraction seam 099 end -// openaiShim test extraction seam 068 start: gitlawb opengateway provider flag sends OPENAI_API_KEY fallback despite stale generic base URL -test('gitlawb opengateway provider flag sends OPENAI_API_KEY fallback despite stale generic base URL', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_API_KEY = 'fake-openai-fallback' - delete process.env.OPENGATEWAY_API_KEY - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() - - const captured = await captureChatCompletionRequest() +// openaiShim test extraction seam 100 start: normalizes streaming Bash arguments that begin with bracket syntax +test('normalizes streaming Bash arguments that begin with bracket syntax', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '[ -f package.json ] && pwd', + }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'tool_calls', + }, + ], + }, + ]) - expect(captured.url).toBe('https://opengateway.gitlawb.com/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-openai-fallback') -}) -// openaiShim test extraction seam 068 end + return makeSseResponse(chunks) + }) as unknown as FetchType + const client = createOpenAIShimClient({}) as OpenAIShimClient -// openaiShim test extraction seam 069 start: gitlawb opengateway provider flag trims OPENGATEWAY_API_KEY before bearer auth -test('gitlawb opengateway provider flag trims OPENGATEWAY_API_KEY before bearer auth', async () => { - process.env.OPENGATEWAY_API_KEY = ' fake-ogw-key ' - delete process.env.OPENAI_API_KEY + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - const captured = await captureChatCompletionRequest() + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - expect(captured.authorization).toBe('Bearer fake-ogw-key') + expect(normalizedInput).toBe('{"command":"[ -f package.json ] && pwd"}') }) -// openaiShim test extraction seam 069 end - - -// openaiShim test extraction seam 070 start: gitlawb opengateway provider flag ignores blank OPENGATEWAY_API_KEY and uses OPENAI_API_KEY fallback -test('gitlawb opengateway provider flag ignores blank OPENGATEWAY_API_KEY and uses OPENAI_API_KEY fallback', async () => { - process.env.OPENGATEWAY_API_KEY = ' ' - process.env.OPENAI_API_KEY = 'fake-openai-fallback' +// openaiShim test extraction seam 100 end - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() - const captured = await captureChatCompletionRequest() +// openaiShim test extraction seam 101 start: normalizes streaming Bash arguments when the first chunk is only an opening brace +test('normalizes streaming Bash arguments when the first chunk is only an opening brace', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '{', + }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + type: 'function', + function: { + arguments: ' pwd; }', + }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'tool_calls', + }, + ], + }, + ]) - expect(captured.authorization).toBe('Bearer fake-openai-fallback') -}) -// openaiShim test extraction seam 070 end + return makeSseResponse(chunks) + }) as unknown as FetchType + const client = createOpenAIShimClient({}) as OpenAIShimClient -// openaiShim test extraction seam 071 start: gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to OPENGATEWAY_BASE_URL override -test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to OPENGATEWAY_BASE_URL override', async () => { - process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' - process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' - delete process.env.OPENAI_API_KEY + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - const captured = await captureChatCompletionRequest() + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-ogw-key') + expect(normalizedInput).toBe('{"command":"{ pwd; }"}') }) -// openaiShim test extraction seam 071 end - - -// openaiShim test extraction seam 072 start: gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to custom OPENAI_BASE_URL fallback -test('gitlawb opengateway provider flag sends OPENGATEWAY_API_KEY to custom OPENAI_BASE_URL fallback', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:8181/v1' - process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' - delete process.env.OPENGATEWAY_BASE_URL - delete process.env.OPENAI_API_KEY +// openaiShim test extraction seam 101 end - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() - const captured = await captureChatCompletionRequest() +// openaiShim test extraction seam 102 start: repairs truncated structured Bash JSON in streaming responses +test('repairs truncated structured Bash JSON in streaming responses', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '{"command":"pwd"', + }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'tool_calls', + }, + ], + }, + ]) - expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-ogw-key') -}) -// openaiShim test extraction seam 072 end + return makeSseResponse(chunks) + }) as unknown as FetchType + const client = createOpenAIShimClient({}) as OpenAIShimClient -// openaiShim test extraction seam 073 start: gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEY for custom base URL -test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEY for custom base URL', async () => { - process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' - process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' - process.env.OPENAI_API_KEY = 'fake-generic-openai-key' + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - const captured = await captureChatCompletionRequest() + const normalizedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-ogw-key') + expect(normalizedInput).toBe('{"command":"pwd"}') }) -// openaiShim test extraction seam 073 end +// openaiShim test extraction seam 102 end -// openaiShim test extraction seam 074 start: gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEYS pool -test('gitlawb opengateway provider flag prefers OPENGATEWAY_API_KEY over generic OPENAI_API_KEYS pool', async () => { - process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' - process.env.OPENGATEWAY_API_KEY = 'fake-ogw-key' - process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' - delete process.env.OPENAI_API_KEY - - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() +// openaiShim test extraction seam 103 start: does not normalize incomplete streamed Bash commands when finish_reason is length +test('does not normalize incomplete streamed Bash commands when finish_reason is length', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: 'rg --fi', + }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'length', + }, + ], + }, + ]) - const captured = await captureChatCompletionRequest() + return makeSseResponse(chunks) + }) as unknown as FetchType - expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-ogw-key') -}) -// openaiShim test extraction seam 074 end + const client = createOpenAIShimClient({}) as OpenAIShimClient -test('longcat provider flag prefers LONGCAT_API_KEY over generic OPENAI_API_KEYS pool', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_API_KEY + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - const result = applyProviderFlag('longcat', []) - expect(result.error).toBeUndefined() + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - const captured = await captureChatCompletionRequest() + const streamedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-longcat-key') + expect(streamedInput).toBe('rg --fi') }) +// openaiShim test extraction seam 103 end -test('longcat provider flag strips unsupported tool definitions', async () => { - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) as Record - return makeChatCompletionResponse('LongCat-2.0') - }) as unknown as FetchType - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_API_KEY +// openaiShim test extraction seam 104 start: repairs truncated JSON objects even without command field +test('repairs truncated JSON objects even without command field', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + tool_calls: [ + { + index: 0, + id: 'function-call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '{"cwd":"/tmp"', + }, + }, + ], + }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'google/gemini-3.1-pro-preview', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'tool_calls', + }, + ], + }, + ]) - expect(applyProviderFlag('longcat', []).error).toBeUndefined() + return makeSseResponse(chunks) + }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'LongCat-2.0', - messages: [ - { role: 'user', content: 'List files' }, - ], - tools: [{ - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, - }], - max_tokens: 32, - stream: false, - }) - expect(requestBody?.tools).toBeUndefined() -}) + const result = await client.beta.messages + .create({ + model: 'google/gemini-3.1-pro-preview', + system: 'test system', + messages: [{ role: 'user', content: 'Use Bash' }], + max_tokens: 64, + stream: true, + }) + .withResponse() -test('longcat rejects image input before dispatch', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_API_KEY + const events: Array> = [] + for await (const event of result.data) { + events.push(event) + } - expect(applyProviderFlag('longcat', []).error).toBeUndefined() + const streamedInput = events + .filter( + event => + event.type === 'content_block_delta' && + typeof event.delta === 'object' && + event.delta !== null && + (event.delta as Record).type === 'input_json_delta', + ) + .map(event => (event.delta as Record).partial_json) + .join('') - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect(client.beta.messages.create({ - model: 'LongCat-2.0', - messages: [{ - role: 'user', - content: [ - { type: 'text', text: 'Describe this image.' }, - { type: 'image', source: { type: 'url', url: 'https://example.com/image.png' } }, - ], - }], - max_tokens: 32, - stream: false, - })).rejects.toThrow('does not support image inputs') + expect(streamedInput).toBe('{"cwd":"/tmp"}') }) +// openaiShim test extraction seam 106 end -test('longcat rejects image tool results before dispatch', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_API_KEY - expect(applyProviderFlag('longcat', []).error).toBeUndefined() - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect(client.beta.messages.create({ - model: 'LongCat-2.0', - messages: [ - { role: 'user', content: 'Inspect the screenshot' }, - { role: 'assistant', content: [{ type: 'tool_use', id: 'call_screenshot', name: 'Screenshot', input: {} }] }, - { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_screenshot', content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'aGVsbG8=' } }] }] }, - ], - tools: [{ name: 'Screenshot', description: 'Capture a screenshot', input_schema: { type: 'object', properties: {} } }], - max_tokens: 32, - stream: false, - })).rejects.toThrow('does not support image inputs') -}) +// Extraction seam: argument parsing | schema sanitation. -test('longcat accepts the documented bare OpenAI SDK base URL', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai' - delete process.env.OPENAI_API_KEY +// openaiShim test extraction seam 107 start: sanitizes malformed MCP tool schemas before sending them to OpenAI - expect(applyProviderFlag('longcat', []).error).toBeUndefined() +// openaiShim test extraction seam 107 end - const captured = await captureChatCompletionRequest() - expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-longcat-key') -}) +// openaiShim test extraction seam 108 start: optional tool properties are not added to required[] — fixes Groq/Azure 400 tool_use_failed -test('longcat accepts the documented bare OpenAI SDK base URL with a trailing slash', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/' - delete process.env.OPENAI_API_KEY +// openaiShim test extraction seam 108 end - expect(applyProviderFlag('longcat', []).error).toBeUndefined() - const captured = await captureChatCompletionRequest() +// Extraction seam: schema sanitation | message conversion façade. - expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-longcat-key') -}) +// --------------------------------------------------------------------------- +// Extraction boundary: tool conversion | message conversion (Issue #202) +// +// Focused suites own the behavior on either side of this boundary. +// This pointer intentionally remains in the façade suite after extraction. +// It also gives independent extraction branches stable merge context. +// +// --------------------------------------------------------------------------- -test('longcat does not append chat completions to a configured endpoint URL', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/v1/chat/completions' - delete process.env.OPENAI_API_KEY - - expect(applyProviderFlag('longcat', []).error).toBeUndefined() - - const captured = await captureChatCompletionRequest() - - expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-longcat-key') -}) - -test('longcat normalizes a configured endpoint URL with a trailing slash', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/v1/chat/completions/' - delete process.env.OPENAI_API_KEY - - expect(applyProviderFlag('longcat', []).error).toBeUndefined() - const captured = await captureChatCompletionRequest() - expect(captured.url).toBe('https://api.longcat.chat/openai/v1/chat/completions') -}) - -test('longcat accepts the documented CodeBuddy endpoint URL', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/chat/completions' - delete process.env.OPENAI_API_KEY - - expect(applyProviderFlag('longcat', []).error).toBeUndefined() - const captured = await captureChatCompletionRequest() - expect(captured.url).toBe('https://api.longcat.chat/openai/chat/completions') -}) - -test('longcat prefers its dedicated credential over a copied key from another provider', async () => { - process.env.LONGCAT_API_KEY = 'fake-longcat-key' - process.env.MIMO_API_KEY = 'fake-mimo-key' - process.env.OPENAI_API_KEY = 'fake-mimo-key' - process.env.OPENAI_BASE_URL = 'https://api.longcat.chat/openai/v1' - - const captured = await captureChatCompletionRequest('LongCat-2.0') - - expect(captured.authorization).toBe('Bearer fake-longcat-key') -}) - -test('longcat provider flag never falls back to an OPENAI_API_KEYS pool', async () => { - process.env.OPENAI_API_KEYS = 'other-provider-secret' - delete process.env.LONGCAT_API_KEY - delete process.env.OPENAI_BASE_URL - delete process.env.OPENAI_API_KEY - - const result = applyProviderFlag('longcat', []) - expect(result.error).toBeUndefined() - - const captured = await captureChatCompletionRequest() - - expect(captured.authorization).not.toBe('Bearer other-provider-secret') +// openaiShim test extraction seam 109 start: the OpenAI shim façade exposes the messages.create contract +test('the OpenAI shim façade exposes the messages.create contract', () => { + const client = createOpenAIShimClient({}) as OpenAIShimClient + expect(typeof client.beta.messages.create).toBe('function') }) +// openaiShim test extraction seam 109 end -test('dedicated-only ClinePass route never falls back to generic OpenAI credentials', async () => { - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.cline.bot/api/v1' - process.env.OPENAI_MODEL = 'cline-pass/deepseek-v4-flash' - process.env.OPENAI_API_KEY = 'generic-openai-key' - process.env.OPENAI_API_KEYS = 'generic-openai-pool-a,generic-openai-pool-b' - delete process.env.CLINE_API_KEY - const captured = await captureChatCompletionRequest( - 'cline-pass/deepseek-v4-flash', +function makeNonStreamResponse(content = 'ok'): Response { + return new Response( + JSON.stringify({ + id: 'chatcmpl-test', + model: 'test-model', + choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }], + usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 }, + }), + { headers: { 'Content-Type': 'application/json' } }, ) +} - expect(captured.authorization).toBeNull() -}) - -// openaiShim test extraction seam 075 start: gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback -test('gitlawb opengateway provider flag uses generic OPENAI_API_KEYS pool before generic OPENAI_API_KEY fallback', async () => { - process.env.OPENGATEWAY_BASE_URL = 'http://localhost:8181/v1' - process.env.OPENAI_API_KEYS = 'fake-openai-pool-a,fake-openai-pool-b' - process.env.OPENAI_API_KEY = 'fake-generic-openai-key' - delete process.env.OPENGATEWAY_API_KEY - - const result = applyProviderFlag('gitlawb-opengateway', []) - expect(result.error).toBeUndefined() - - const captured = await captureChatCompletionRequest() - - expect(captured.url).toBe('http://localhost:8181/v1/chat/completions') - expect(captured.authorization).toBe('Bearer fake-openai-pool-a') -}) -// openaiShim test extraction seam 075 end +// openaiShim test extraction seam 110 start: coalesces consecutive user messages to avoid alternation errors (issue #202) +test('coalesces consecutive user messages to avoid alternation errors (issue #202)', async () => { + let sentMessages: Array<{ role: string; content: unknown }> | undefined + globalThis.fetch = (async (_input: unknown, init: RequestInit | undefined) => { + sentMessages = JSON.parse(String(init?.body)).messages + return makeNonStreamResponse() + }) as unknown as FetchType -// openaiShim test extraction seam 076 start: gitlawb opengateway stored provider profile key becomes bearer auth -test('gitlawb opengateway stored provider profile key becomes bearer auth', async () => { - delete process.env.OPENAI_API_KEY - delete process.env.OPENGATEWAY_API_KEY + const client = createOpenAIShimClient({}) as OpenAIShimClient - applyProviderProfileToProcessEnv({ - id: 'stored-opengateway', - provider: 'gitlawb-opengateway', - name: 'Gitlawb Opengateway', - baseUrl: 'https://opengateway.gitlawb.com/v1', - model: 'mimo-v2.5-pro', - apiKey: 'fake-profile-key', + await client.beta.messages.create({ + model: 'test-model', + system: 'sys', + messages: [ + { role: 'user', content: 'first message' }, + { role: 'user', content: 'second message' }, + ], + max_tokens: 64, + stream: false, }) - const captured = await captureChatCompletionRequest() - - expect(captured.authorization).toBe('Bearer fake-profile-key') -}) -// openaiShim test extraction seam 076 end - - -// openaiShim test extraction seam 077 start: openai route still sends OPENAI_API_KEY as bearer auth -test('openai route still sends OPENAI_API_KEY as bearer auth', async () => { - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEY = 'fake-openai-key' - delete process.env.OPENGATEWAY_API_KEY - - const captured = await captureChatCompletionRequest('gpt-5.5') - - expect(captured.authorization).toBe('Bearer fake-openai-key') + expect(sentMessages?.length).toBe(2) + expect(sentMessages?.[0]?.role).toBe('system') + expect(sentMessages?.[1]?.role).toBe('user') + const userContent = sentMessages?.[1]?.content as string + expect(userContent).toContain('first message') + expect(userContent).toContain('second message') }) -// openaiShim test extraction seam 077 end - +// openaiShim test extraction seam 110 end -// openaiShim test extraction seam 078 start: OPENAI_API_KEYS rejects placeholder values before sending requests -test('OPENAI_API_KEYS rejects placeholder values before sending requests', async () => { - const authorizations: Array = [] - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,SUA_CHAVE' - process.env.OPENAI_API_KEY = 'single-key-should-not-hide-invalid-pool' +// openaiShim test extraction seam 111 start: coalesces consecutive assistant messages preserving tool_calls (issue #202) +test('coalesces consecutive assistant messages preserving tool_calls (issue #202)', async () => { + let sentMessages: Array<{ role: string; content: unknown; tool_calls?: unknown[] }> | undefined - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - return makeChatCompletionResponse('gpt-5.5') + globalThis.fetch = (async (_input: unknown, init: RequestInit | undefined) => { + sentMessages = JSON.parse(String(init?.body)).messages + return makeNonStreamResponse() }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow(/SUA_CHAVE|Authentication failed/) - - expect(authorizations).toEqual([]) -}) -// openaiShim test extraction seam 078 end - -// openaiShim test extraction seam 079 start: OPENAI_API_KEYS rotates to the next key on rate-limit failure -test('OPENAI_API_KEYS rotates to the next key on rate-limit failure', async () => { - const authorizations: Array = [] - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - process.env.OPENAI_API_KEY = 'single-key-should-not-win' - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - - if (authorizations.length === 1) { - return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { - status: 429, - headers: { 'Content-Type': 'application/json' }, - }) - } - - return makeChatCompletionResponse('gpt-5.5') - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, + model: 'test-model', + system: 'sys', + messages: [ + { role: 'user', content: 'go' }, + { role: 'assistant', content: 'thinking...' }, + { + role: 'assistant', + content: [{ type: 'tool_use', id: 'call_1', name: 'Bash', input: { command: 'ls' } }], + }, + { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'file.txt' }] }, + ], + max_tokens: 64, stream: false, }) - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) + const assistantMsgs = sentMessages?.filter(m => m.role === 'assistant') + expect(assistantMsgs?.length).toBe(1) + expect(assistantMsgs?.[0]?.tool_calls?.length).toBeGreaterThan(0) }) -// openaiShim test extraction seam 079 end +// openaiShim test extraction seam 111 end +// --------------------------------------------------------------------------- +// Extraction boundary: message conversion | non-streaming response conversion +// +// Focused suites own the behavior on either side of this boundary. +// This pointer intentionally remains in the façade suite after extraction. +// It also gives independent extraction branches stable merge context. +// +// --------------------------------------------------------------------------- -// openaiShim test extraction seam 080 start: OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited -test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { - const authorizations: Array = [] +// openaiShim test extraction seam 112 start: the OpenAI shim façade creates independent client instances +test('the OpenAI shim façade creates independent client instances', () => { + const first = createOpenAIShimClient({}) as OpenAIShimClient + const second = createOpenAIShimClient({}) as OpenAIShimClient + expect(first).not.toBe(second) + expect(first.beta).not.toBe(second.beta) + expect(first.beta.messages).not.toBe(second.beta.messages) +}) +// openaiShim test extraction seam 112 end - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - delete process.env.OPENAI_API_KEY +test('raw-text and XML fallback tool calls use one unique sequence', () => { + const text = parseTextToolCalls('{"name":"from_text","arguments":{}}') + const xml = parseXmlToolCalls('{"name":"from_xml","arguments":{}}') + expect(text.calls[0]?.id).toMatch(/^ollama_tc_\d+$/) + expect(xml.calls[0]?.id).toMatch(/^xml_tc_\d+$/) + const textNum = Number(text.calls[0]?.id?.replace(/^\D+/, '')) + const xmlNum = Number(xml.calls[0]?.id?.replace(/^\D+/, '')) + // Same session counter: the second mint must be exactly one greater than the first. + expect(xmlNum).toBe(textNum + 1) +}) - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { - status: 429, - headers: { 'Content-Type': 'application/json' }, - }) +// --------------------------------------------------------------------------- +// openaiShim test extraction seam 113 start: non-streaming: reasoning_content emitted as thinking block only when content is null +test('non-streaming: reasoning_content emitted as thinking block only when content is null', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5', + choices: [ + { + message: { + role: 'assistant', + content: null, + reasoning_content: 'Let me think about this step by step.', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) -}) -// openaiShim test extraction seam 080 end + const result = (await client.beta.messages.create({ + model: 'glm-5', + system: 'test system', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + })) as { content: Array> } + expect(result.content).toEqual([ + { type: 'thinking', thinking: 'Let me think about this step by step.' }, + ]) +}) +// openaiShim test extraction seam 113 end -// openaiShim test extraction seam 081 start: comma-separated OPENAI_API_KEY rotates to the next key on rate-limit failure -test('comma-separated OPENAI_API_KEY rotates to the next key on rate-limit failure', async () => { - const authorizations: Array = [] - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEY = 'key-a,key-b' - delete process.env.OPENAI_API_KEYS - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - - if (authorizations.length === 1) { - return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { - status: 429, - headers: { 'Content-Type': 'application/json' }, - }) - } - - return makeChatCompletionResponse('gpt-5.5') - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) -}) -// openaiShim test extraction seam 081 end - - -// openaiShim test extraction seam 082 start: OPENAI_API_KEYS does not rotate through pool on provider 5xx outage -test('OPENAI_API_KEYS does not rotate through pool on provider 5xx outage', async () => { - const authorizations: Array = [] - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - delete process.env.OPENAI_API_KEY - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - - return new Response(JSON.stringify({ error: { message: 'server error' } }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() - - expect(authorizations).toEqual(['Bearer key-a']) -}) -// openaiShim test extraction seam 082 end - -// openaiShim test extraction seam 083 start: OPENAI_API_KEYS preserves cooldown state across client requests -test('OPENAI_API_KEYS preserves cooldown state across client requests', async () => { - const authorizations: Array = [] - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - - if (authorizations.length === 1) { - return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { - status: 429, - headers: { 'Content-Type': 'application/json' }, - }) - } - - return makeChatCompletionResponse('gpt-5.5') +// openaiShim test extraction seam 114 start: non-streaming: empty string content does not fall through to reasoning_content as text +test('non-streaming: empty string content does not fall through to reasoning_content as text', async () => { + globalThis.fetch = (async (_input, _init) => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'glm-5', + choices: [ + { + message: { + role: 'assistant', + content: '', + reasoning_content: 'Chain of thought here.', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - for (let i = 0; i < 2; i++) { - await client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) - } - - expect(authorizations).toEqual([ - 'Bearer key-a', - 'Bearer key-b', - 'Bearer key-b', - ]) -}) -// openaiShim test extraction seam 083 end - - -// openaiShim test extraction seam 084 start: OPENAI_API_KEYS rotates Azure api-key auth on auth failure -test('OPENAI_API_KEYS rotates Azure api-key auth on auth failure', async () => { - const apiKeys: Array = [] - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://example.openai.azure.com/openai/deployments/test/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'azure-key-a,azure-key-b' - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - apiKeys.push(headers?.['api-key'] ?? null) - - if (apiKeys.length === 1) { - return new Response(JSON.stringify({ error: { message: 'unauthorized' } }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }) - } - - return makeChatCompletionResponse('gpt-5.5') - }) as unknown as FetchType - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'gpt-5.5', + const result = (await client.beta.messages.create({ + model: 'glm-5', + system: 'test system', messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, + max_tokens: 64, stream: false, - }) - - expect(apiKeys).toEqual(['azure-key-a', 'azure-key-b']) -}) -// openaiShim test extraction seam 084 end - - -// openaiShim test extraction seam 085 start: OPENAI_API_KEYS does not reuse auth-disabled credentials across client requests -test('OPENAI_API_KEYS does not reuse auth-disabled credentials across client requests', async () => { - const authorizations: Array = [] - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - delete process.env.OPENAI_API_KEY - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - - return new Response(JSON.stringify({ error: { message: 'unauthorized' } }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() - - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello again' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() - - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) -}) -// openaiShim test extraction seam 085 end - - -// openaiShim test extraction seam 086 start: OPENAI_API_KEYS permanently evicts 403 auth failures -test('OPENAI_API_KEYS permanently evicts 403 auth failures', async () => { - const authorizations: Array = [] - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_MODEL = 'gpt-5.5' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - delete process.env.OPENAI_API_KEY - - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - - return new Response(JSON.stringify({ error: { message: 'forbidden' } }), { - status: 403, - headers: { 'Content-Type': 'application/json' }, - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() - - await expect( - client.beta.messages.create({ - model: 'gpt-5.5', - messages: [{ role: 'user', content: 'hello again' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() + })) as { content: Array> } - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) + expect(result.content).toEqual([ + { type: 'thinking', thinking: 'Chain of thought here.' }, + ]) }) -// openaiShim test extraction seam 086 end - -// openaiShim test extraction seam 087 start: does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes -test('does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes', async () => { - let capturedAuthorization: string | null = null - - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' - process.env.OPENAI_MODEL = 'openai/gpt-5-mini' - process.env.BNKR_API_KEY = 'bankr-live-key' - delete process.env.OPENAI_API_KEY - delete process.env.OPENROUTER_API_KEY +// openaiShim test extraction seam 114 end - globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - capturedAuthorization = - headers?.Authorization ?? headers?.authorization ?? null +// openaiShim test extraction seam 115 start: non-streaming: real content takes precedence over reasoning_content +test('non-streaming: real content takes precedence over reasoning_content', async () => { + globalThis.fetch = (async (_input, _init) => { return new Response( JSON.stringify({ - id: 'chatcmpl-openrouter', - model: 'openai/gpt-5-mini', + id: 'chatcmpl-1', + model: 'glm-5', choices: [ { message: { role: 'assistant', - content: 'ok', + content: 'The answer is 42.', + reasoning_content: 'I need to calculate this.', }, finish_reason: 'stop', }, ], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + }, }), { headers: { @@ -5134,78 +3928,116 @@ test('does not use BNKR_API_KEY for non-Bankr OpenAI-compatible routes', async ( const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'openai/gpt-5-mini', + const result = (await client.beta.messages.create({ + model: 'glm-5', + system: 'test system', messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, + max_tokens: 64, stream: false, - }) + })) as { content: Array> } - expect(capturedAuthorization).toBeNull() + expect(result.content).toEqual([ + { type: 'thinking', thinking: 'I need to calculate this.' }, + { type: 'text', text: 'The answer is 42.' }, + ]) }) -// openaiShim test extraction seam 087 end - - -// openaiShim test extraction seam 088 start: preserves Gemini tool call extra_content from streaming chunks - -// openaiShim test extraction seam 088 end - - -// openaiShim test extraction seam 089 start: preserves Gemini thought signature from streaming delta extra_content - -// openaiShim test extraction seam 089 end - - -// openaiShim test extraction seam 090 start: preserves Gemini thought signature from non-streaming message extra_content - -// openaiShim test extraction seam 090 end - +// openaiShim test extraction seam 117 end -// Extraction seam: provider signature metadata | raw streaming tool fallback. -// openaiShim test extraction seam 091 start: converts Gemini raw tool-call text into streaming tool_use blocks -test('converts Gemini raw tool-call text into streaming tool_use blocks', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-raw-tool', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-flash-lite', +// openaiShim test extraction seam 118 start: non-streaming: strips tag block from assistant content +test('non-streaming: strips tag block from assistant content', async () => { + globalThis.fetch = asMockFetch(mock(async () => { + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-5-mini', choices: [ { - index: 0, - delta: { + message: { role: 'assistant', - content: 'Tool calls', + content: + 'user wants a greeting, respond brieflyHey! How can I help you today?', }, - finish_reason: null, + finish_reason: 'stop', }, ], - }, - { - id: 'chatcmpl-raw-tool', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-flash-lite', - choices: [ - { - index: 0, - delta: { - content: - ' requested:\n- Write({"file_path":"style.css","content":"ul { padding: 0; }"}) [id: call79435b5a26564619b0151197]', + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + })) + + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = (await client.beta.messages.create({ + model: 'gpt-5-mini', + system: 'test system', + messages: [{ role: 'user', content: 'hey' }], + max_tokens: 64, + stream: false, + })) as { content: Array> } + + expect(result.content).toEqual([ + { type: 'text', text: 'Hey! How can I help you today?' }, + ]) +}) +// openaiShim test extraction seam 118 end + + +// Extraction seam: non-streaming response conversion | streaming event conversion. + +// openaiShim test extraction seam 119 start: streaming: thinking block closed before tool call +test('streaming: thinking block closed before tool call', async () => { + globalThis.fetch = (async (_input, _init) => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5', + choices: [ + { + index: 0, + delta: { role: 'assistant', reasoning_content: 'Thinking...' }, + finish_reason: null, + }, + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'glm-5', + choices: [ + { + index: 0, + delta: { + tool_calls: [ + { + index: 0, + id: 'call-1', + type: 'function', + function: { + name: 'Bash', + arguments: '{"command":"ls"}', + }, + }, + ], }, finish_reason: null, }, ], }, { - id: 'chatcmpl-raw-tool', + id: 'chatcmpl-1', object: 'chat.completion.chunk', - model: 'google/gemini-3.1-flash-lite', + model: 'glm-5', choices: [ { index: 0, delta: {}, - finish_reason: 'stop', + finish_reason: 'tool_calls', }, ], }, @@ -5218,8 +4050,9 @@ test('converts Gemini raw tool-call text into streaming tool_use blocks', async const result = await client.beta.messages .create({ - model: 'google/gemini-3.1-flash-lite', - messages: [{ role: 'user', content: 'Write CSS' }], + model: 'glm-5', + system: 'test system', + messages: [{ role: 'user', content: 'Run ls' }], max_tokens: 64, stream: true, }) @@ -5230,400 +4063,130 @@ test('converts Gemini raw tool-call text into streaming tool_use blocks', async events.push(event) } - expect( - events.some( - event => - event.type === 'content_block_start' && - (event.content_block as Record | undefined)?.type === - 'text', - ), - ).toBe(false) + const types = events.map(e => e.type) - const toolStart = events.find( - event => - event.type === 'content_block_start' && - (event.content_block as Record | undefined)?.type === - 'tool_use', - ) as { content_block?: Record } | undefined - expect(toolStart?.content_block).toMatchObject({ - type: 'tool_use', - id: 'call79435b5a26564619b0151197', - name: 'Write', - }) + const thinkingStartIdx = types.indexOf('content_block_start') + const firstStopIdx = types.indexOf('content_block_stop') + const toolStartIdx = types.indexOf( + 'content_block_start', + thinkingStartIdx + 1, + ) - const toolInput = events - .filter( - event => - event.type === 'content_block_delta' && - (event.delta as Record | undefined)?.type === - 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - expect(JSON.parse(toolInput)).toEqual({ - file_path: 'style.css', - content: 'ul { padding: 0; }', - }) + expect(thinkingStartIdx).toBeGreaterThanOrEqual(0) + expect(firstStopIdx).toBeGreaterThan(thinkingStartIdx) + expect(toolStartIdx).toBeGreaterThan(firstStopIdx) - const stop = events.find(event => event.type === 'message_delta') as - | { delta?: Record } - | undefined - expect(stop?.delta?.stop_reason).toBe('tool_use') + const thinkingStart = events[thinkingStartIdx] as { + content_block?: Record + } + expect(thinkingStart?.content_block?.type).toBe('thinking') }) -// openaiShim test extraction seam 091 end - +// openaiShim test extraction seam 119 end -// Extraction seam: streaming conversion | non-streaming response conversion. -// openaiShim test extraction seam 092 start: converts Gemini raw tool-call text into non-streaming tool_use blocks -test('converts Gemini raw tool-call text into non-streaming tool_use blocks', async () => { - const previousFetch = globalThis.fetch - try { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-raw-tool', - model: 'google/gemini-3.1-flash-lite', - choices: [ - { - message: { - role: 'assistant', - content: - 'Tool calls requested:\n- Agent({"description":"Verify the todo list application functionality.","prompt":"Check files.","subagent_type":"verification"}) [id: call9a8b7c6d5e4f3a2b1c0d9e8f]', - }, - finish_reason: 'stop', +// openaiShim test extraction seam 120 start: streaming: strips tag block from assistant content deltas +test('streaming: strips tag block from assistant content deltas', async () => { + globalThis.fetch = asMockFetch(mock(async () => { + const chunks = makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'gpt-5-mini', + choices: [ + { + index: 0, + delta: { + role: 'assistant', + content: + 'user wants a greeting, respond brieflyHey! How can I help you today?', }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, + finish_reason: null, }, - }), - { - headers: { - 'Content-Type': 'application/json', + ], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model: 'gpt-5-mini', + choices: [ + { + index: 0, + delta: {}, + finish_reason: 'stop', }, - }, - ) - }) as unknown as FetchType + ], + }, + ]) - const client = createOpenAIShimClient({}) as OpenAIShimClient + return makeSseResponse(chunks) + })) - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-flash-lite', - messages: [{ role: 'user', content: 'Verify' }], + const client = createOpenAIShimClient({}) as OpenAIShimClient + const result = await client.beta.messages + .create({ + model: 'gpt-5-mini', + system: 'test system', + messages: [{ role: 'user', content: 'hey' }], max_tokens: 64, - stream: false, - }) as { - stop_reason?: string - content?: Array> - } + stream: true, + }) + .withResponse() - expect(message.stop_reason).toBe('tool_use') - expect(message.content).toEqual([ - { - type: 'tool_use', - id: 'call9a8b7c6d5e4f3a2b1c0d9e8f', - name: 'Agent', - input: { - description: 'Verify the todo list application functionality.', - prompt: 'Check files.', - subagent_type: 'verification', - }, - }, - ]) - } finally { - globalThis.fetch = previousFetch + const textDeltas: string[] = [] + for await (const event of result.data) { + const delta = (event as { delta?: { type?: string; text?: string } }).delta + if (delta?.type === 'text_delta' && typeof delta.text === 'string') { + textDeltas.push(delta.text) + } } + + expect(textDeltas.join('')).toBe('Hey! How can I help you today?') }) -// openaiShim test extraction seam 092 end +// openaiShim test extraction seam 120 end -// openaiShim test extraction seam 093 start: normalizes plain string Bash tool arguments from OpenAI-compatible responses -test('normalizes plain string Bash tool arguments from OpenAI-compatible responses', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ +// openaiShim test extraction seam 121 start: streaming: strips tag split across multiple content chunks +test('streaming: strips tag split across multiple content chunks', async () => { + globalThis.fetch = asMockFetch(mock(async () => { + const chunks = makeStreamChunks([ + { id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', + object: 'chat.completion.chunk', + model: 'gpt-5-mini', choices: [ { - message: { + index: 0, + delta: { role: 'assistant', - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: 'pwd', - }, - }, - ], + content: 'user wants a greeting,', }, - finish_reason: 'tool_calls', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: false, - }) as { - stop_reason?: string - content?: Array> - } - - expect(message.stop_reason).toBe('tool_use') - expect(message.content).toEqual([ - { - type: 'tool_use', - id: 'function-call-1', - name: 'Bash', - input: { command: 'pwd' }, - }, - ]) -}) -// openaiShim test extraction seam 093 end - - -// openaiShim test extraction seam 094 start: normalizes Bash tool arguments that are valid JSON strings -test('normalizes Bash tool arguments that are valid JSON strings', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - message: { - role: 'assistant', - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '"pwd"', - }, - }, - ], - }, - finish_reason: 'tool_calls', + finish_reason: null, }, ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: false, - }) as { - content?: Array> - } - - expect(message.content).toEqual([ - { - type: 'tool_use', - id: 'function-call-1', - name: 'Bash', - input: { command: 'pwd' }, - }, - ]) -}) -// openaiShim test extraction seam 094 end - - -test.each([ - ['false', false], - ['null', null], - ['[]', []], -])( - 'preserves malformed Bash JSON literals as parsed values in non-streaming responses: %s', - async (argumentsValue, expectedInput) => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - message: { - role: 'assistant', - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: argumentsValue, - }, - }, - ], - }, - finish_reason: 'tool_calls', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: false, - }) as { - content?: Array> - } - - expect(message.content).toEqual([ { - type: 'tool_use', - id: 'function-call-1', - name: 'Bash', - input: expectedInput, - }, - ]) - }, -) - -// openaiShim test extraction seam 095 start: keeps terminal empty Bash tool arguments invalid in non-streaming responses -test('keeps terminal empty Bash tool arguments invalid in non-streaming responses', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', + object: 'chat.completion.chunk', + model: 'gpt-5-mini', choices: [ { - message: { - role: 'assistant', - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '', - }, - }, - ], + index: 0, + delta: { + content: ' respond briefly> - } - - expect(message.content).toEqual([ - { - type: 'tool_use', - id: 'function-call-1', - name: 'Bash', - input: {}, - }, - ]) -}) -// openaiShim test extraction seam 095 end - - -// Extraction seam: completed tool parsing | streamed tool normalization. - -// openaiShim test extraction seam 096 start: normalizes plain string Bash tool arguments in streaming responses -test('normalizes plain string Bash tool arguments in streaming responses', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ { id: 'chatcmpl-1', object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', + model: 'gpt-5-mini', choices: [ { index: 0, delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: 'pwd', - }, - }, - ], + content: 'ink>Hey! How can I help you today?', }, finish_reason: null, }, @@ -5632,77 +4195,62 @@ test('normalizes plain string Bash tool arguments in streaming responses', async { id: 'chatcmpl-1', object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', + model: 'gpt-5-mini', choices: [ { index: 0, delta: {}, - finish_reason: 'tool_calls', + finish_reason: 'stop', }, ], }, ]) return makeSseResponse(chunks) - }) as unknown as FetchType + })) const client = createOpenAIShimClient({}) as OpenAIShimClient const result = await client.beta.messages .create({ - model: 'google/gemini-3.1-pro-preview', + model: 'gpt-5-mini', system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], + messages: [{ role: 'user', content: 'hey' }], max_tokens: 64, stream: true, }) .withResponse() - const events: Array> = [] + const textDeltas: string[] = [] for await (const event of result.data) { - events.push(event) + const delta = (event as { delta?: { type?: string; text?: string } }).delta + if (delta?.type === 'text_delta' && typeof delta.text === 'string') { + textDeltas.push(delta.text) + } } - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(normalizedInput).toBe('{"command":"pwd"}') + expect(textDeltas.join('')).toBe('Hey! How can I help you today?') }) -// openaiShim test extraction seam 096 end +// openaiShim test extraction seam 121 end -// openaiShim test extraction seam 097 start: normalizes plain string Bash tool arguments when streaming starts with an empty chunk -test('normalizes plain string Bash tool arguments when streaming starts with an empty chunk', async () => { - globalThis.fetch = (async (_input, _init) => { +// openaiShim test extraction seam 122 start: streaming: preserves prose without tags (no phrase-based false positive) +test('streaming: preserves prose without tags (no phrase-based false positive)', async () => { + // Regression: older phrase-based sanitizer would strip "I should..." prose. + // The tag-based approach leaves legitimate assistant output alone. + globalThis.fetch = asMockFetch(mock(async () => { const chunks = makeStreamChunks([ { id: 'chatcmpl-1', object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', + model: 'gpt-5-mini', choices: [ { index: 0, delta: { role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '', - }, - }, - ], + content: + 'I should note that the user role requires a briefly concise friendly response format.', }, finish_reason: null, }, @@ -5711,4913 +4259,978 @@ test('normalizes plain string Bash tool arguments when streaming starts with an { id: 'chatcmpl-1', object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: 0, - type: 'function', - function: { - arguments: 'pwd', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', + model: 'gpt-5-mini', choices: [ { index: 0, delta: {}, - finish_reason: 'tool_calls', + finish_reason: 'stop', }, ], }, ]) return makeSseResponse(chunks) - }) as unknown as FetchType + })) const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages .create({ - model: 'google/gemini-3.1-pro-preview', + model: 'gpt-5-mini', system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], + messages: [{ role: 'user', content: 'hey' }], max_tokens: 64, stream: true, }) .withResponse() - const events: Array> = [] + const textDeltas: string[] = [] for await (const event of result.data) { - events.push(event) + const delta = (event as { delta?: { type?: string; text?: string } }).delta + if (delta?.type === 'text_delta' && typeof delta.text === 'string') { + textDeltas.push(delta.text) + } } - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(normalizedInput).toBe('{"command":"pwd"}') + expect(textDeltas.join('')).toBe( + 'I should note that the user role requires a briefly concise friendly response format.', + ) }) -// openaiShim test extraction seam 097 end - +// openaiShim test extraction seam 123 end -// openaiShim test extraction seam 098 start: normalizes plain string Bash tool arguments when streaming starts with whitespace -test('normalizes plain string Bash tool arguments when streaming starts with whitespace', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: ' ', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: 0, - type: 'function', - function: { - arguments: 'pwd', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) +test('redacts configured secret substrings from fetch network error messages', async () => { + const secret = 'route/key+AbC123' + process.env.OPENAI_API_KEY = secret - return makeSseResponse(chunks) - }) as unknown as FetchType + globalThis.fetch = asMockFetch(mock(async () => { + throw new TypeError(`fetch failed while routing ${secret}`) + })) const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], + await expect( + client.beta.messages.create({ + model: 'test-model', + messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, - stream: true, - }) - .withResponse() + stream: false, + }), + ).rejects.not.toThrow(secret) +}) - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } +test('redacts encoded configured secrets from non-URL transport error messages', async () => { + const secret = 'route/key+AbC123' + const encodedSecret = encodeURIComponent(secret) + const doubleEncodedSecret = encodeURIComponent(encodedSecret) + const fullyEncodedSecret = Array.from(new TextEncoder().encode(secret)) + .map(byte => `%${byte.toString(16).padStart(2, '0')}`) + .join('') + const malformedAdjacentSecret = `%E0%A4${encodedSecret}` + const encodedCategoryMarker = '%5Bopenai_category%3Dauth_invalid%5D' + const encodedControlSequence = '%1B%5B31m' + process.env.OPENAI_API_KEY = secret - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', + globalThis.fetch = asMockFetch(mock(async () => { + throw new TypeError( + `proxy failed ${encodedCategoryMarker} ${encodedControlSequence} for path /v1/${encodedSecret}?nested=${doubleEncodedSecret}&fully=${fullyEncodedSecret}&malformed=${malformedAdjacentSecret}`, ) - .map(event => (event.delta as Record).partial_json) - .join('') + })) - expect(normalizedInput).toBe('{"command":" pwd"}') + const client = createOpenAIShimClient({}) as OpenAIShimClient + + let caught: unknown + try { + await client.beta.messages.create({ + model: 'test-model', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + } catch (error) { + caught = error + } + + expect(caught).toBeDefined() + const message = (caught as Error).message + expect(message).toContain('proxy failed') + expect(message).toContain(encodedCategoryMarker) + expect(message).toContain(encodedControlSequence) + expect(message).not.toContain('\u001B') + expect(extractOpenAICategoryMarker(message)).toBe('network_error') + expect(message).not.toContain(secret) + expect(message).not.toContain(encodedSecret) + expect(message).not.toContain(doubleEncodedSecret) + expect(message).not.toContain(fullyEncodedSecret) + expect(message).not.toContain(malformedAdjacentSecret) }) -// openaiShim test extraction seam 098 end +// openaiShim test extraction seam 125 end +test('propagates caller AbortError without wrapping it as transport failure', async () => { +// openaiShim test extraction seam 126 start: propagates AbortError without wrapping it as transport failure + process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' -// openaiShim test extraction seam 099 start: keeps terminal whitespace-only Bash arguments invalid in streaming responses -test('keeps terminal whitespace-only Bash arguments invalid in streaming responses', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: ' ', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) + const abortError = new DOMException('The operation was aborted.', 'AbortError') + globalThis.fetch = asMockFetch(mock(async () => { + throw abortError + })) - return makeSseResponse(chunks) - }) as unknown as FetchType + const controller = new AbortController() + const callerReason = new DOMException('Cancelled by caller', 'AbortError') + controller.abort(callerReason) const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(normalizedInput).toBe('{}') -}) -// openaiShim test extraction seam 099 end - - -// openaiShim test extraction seam 100 start: normalizes streaming Bash arguments that begin with bracket syntax -test('normalizes streaming Bash arguments that begin with bracket syntax', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '[ -f package.json ] && pwd', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(normalizedInput).toBe('{"command":"[ -f package.json ] && pwd"}') -}) -// openaiShim test extraction seam 100 end - - -// openaiShim test extraction seam 101 start: normalizes streaming Bash arguments when the first chunk is only an opening brace -test('normalizes streaming Bash arguments when the first chunk is only an opening brace', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '{', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: 0, - type: 'function', - function: { - arguments: ' pwd; }', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(normalizedInput).toBe('{"command":"{ pwd; }"}') -}) -// openaiShim test extraction seam 101 end - - -// openaiShim test extraction seam 102 start: repairs truncated structured Bash JSON in streaming responses -test('repairs truncated structured Bash JSON in streaming responses', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '{"command":"pwd"', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const normalizedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(normalizedInput).toBe('{"command":"pwd"}') -}) -// openaiShim test extraction seam 102 end - - -// openaiShim test extraction seam 103 start: does not normalize incomplete streamed Bash commands when finish_reason is length -test('does not normalize incomplete streamed Bash commands when finish_reason is length', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: 'rg --fi', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'length', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const streamedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(streamedInput).toBe('rg --fi') -}) -// openaiShim test extraction seam 103 end - - -// openaiShim test extraction seam 104 start: repairs truncated JSON objects even without command field -test('repairs truncated JSON objects even without command field', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - tool_calls: [ - { - index: 0, - id: 'function-call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '{"cwd":"/tmp"', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use Bash' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const streamedInput = events - .filter( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) - .map(event => (event.delta as Record).partial_json) - .join('') - - expect(streamedInput).toBe('{"cwd":"/tmp"}') -}) -// openaiShim test extraction seam 104 end - - -// Extraction seam: streamed tool normalization | schema and tool conversion. - -// openaiShim test extraction seam 105 start: preserves raw input for unknown plain string tool arguments -test('preserves raw input for unknown plain string tool arguments', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - message: { - role: 'assistant', - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'UnknownTool', - arguments: 'pwd', - }, - }, - ], - }, - finish_reason: 'tool_calls', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use tool' }], - max_tokens: 64, - stream: false, - }) as { - content?: Array> - } - - expect(message.content).toEqual([ - { - type: 'tool_use', - id: 'function-call-1', - name: 'UnknownTool', - input: {}, - }, - ]) -}) -// openaiShim test extraction seam 105 end - - -// openaiShim test extraction seam 106 start: preserves parsed string input for unknown JSON string tool arguments -test('preserves parsed string input for unknown JSON string tool arguments', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'google/gemini-3.1-pro-preview', - choices: [ - { - message: { - role: 'assistant', - tool_calls: [ - { - id: 'function-call-1', - type: 'function', - function: { - name: 'UnknownTool', - arguments: '"pwd"', - }, - }, - ], - }, - finish_reason: 'tool_calls', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const message = await client.beta.messages.create({ - model: 'google/gemini-3.1-pro-preview', - system: 'test system', - messages: [{ role: 'user', content: 'Use tool' }], - max_tokens: 64, - stream: false, - }) as { - content?: Array> - } - - expect(message.content).toEqual([ - { - type: 'tool_use', - id: 'function-call-1', - name: 'UnknownTool', - input: 'pwd', - }, - ]) -}) -// openaiShim test extraction seam 106 end - - -// Extraction seam: argument parsing | schema sanitation. - -// openaiShim test extraction seam 107 start: sanitizes malformed MCP tool schemas before sending them to OpenAI - -// openaiShim test extraction seam 107 end - - -// openaiShim test extraction seam 108 start: optional tool properties are not added to required[] — fixes Groq/Azure 400 tool_use_failed - -// openaiShim test extraction seam 108 end - - -// Extraction seam: schema sanitation | message conversion façade. - -// --------------------------------------------------------------------------- -// Extraction boundary: tool conversion | message conversion (Issue #202) -// -// Focused suites own the behavior on either side of this boundary. -// This pointer intentionally remains in the façade suite after extraction. -// It also gives independent extraction branches stable merge context. -// -// --------------------------------------------------------------------------- - -// openaiShim test extraction seam 109 start: the OpenAI shim façade exposes the messages.create contract -test('the OpenAI shim façade exposes the messages.create contract', () => { - const client = createOpenAIShimClient({}) as OpenAIShimClient - expect(typeof client.beta.messages.create).toBe('function') -}) -// openaiShim test extraction seam 109 end - - -function makeNonStreamResponse(content = 'ok'): Response { - return new Response( - JSON.stringify({ - id: 'chatcmpl-test', - model: 'test-model', - choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }], - usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) -} - -// openaiShim test extraction seam 110 start: coalesces consecutive user messages to avoid alternation errors (issue #202) -test('coalesces consecutive user messages to avoid alternation errors (issue #202)', async () => { - let sentMessages: Array<{ role: string; content: unknown }> | undefined - - globalThis.fetch = (async (_input: unknown, init: RequestInit | undefined) => { - sentMessages = JSON.parse(String(init?.body)).messages - return makeNonStreamResponse() - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'test-model', - system: 'sys', - messages: [ - { role: 'user', content: 'first message' }, - { role: 'user', content: 'second message' }, - ], - max_tokens: 64, - stream: false, - }) - - expect(sentMessages?.length).toBe(2) - expect(sentMessages?.[0]?.role).toBe('system') - expect(sentMessages?.[1]?.role).toBe('user') - const userContent = sentMessages?.[1]?.content as string - expect(userContent).toContain('first message') - expect(userContent).toContain('second message') -}) -// openaiShim test extraction seam 110 end - - -// openaiShim test extraction seam 111 start: coalesces consecutive assistant messages preserving tool_calls (issue #202) -test('coalesces consecutive assistant messages preserving tool_calls (issue #202)', async () => { - let sentMessages: Array<{ role: string; content: unknown; tool_calls?: unknown[] }> | undefined - - globalThis.fetch = (async (_input: unknown, init: RequestInit | undefined) => { - sentMessages = JSON.parse(String(init?.body)).messages - return makeNonStreamResponse() - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'test-model', - system: 'sys', - messages: [ - { role: 'user', content: 'go' }, - { role: 'assistant', content: 'thinking...' }, - { - role: 'assistant', - content: [{ type: 'tool_use', id: 'call_1', name: 'Bash', input: { command: 'ls' } }], - }, - { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_1', content: 'file.txt' }] }, - ], - max_tokens: 64, - stream: false, - }) - - const assistantMsgs = sentMessages?.filter(m => m.role === 'assistant') - expect(assistantMsgs?.length).toBe(1) - expect(assistantMsgs?.[0]?.tool_calls?.length).toBeGreaterThan(0) -}) -// openaiShim test extraction seam 111 end - -// --------------------------------------------------------------------------- -// Extraction boundary: message conversion | non-streaming response conversion -// -// Focused suites own the behavior on either side of this boundary. -// This pointer intentionally remains in the façade suite after extraction. -// It also gives independent extraction branches stable merge context. -// -// --------------------------------------------------------------------------- - -// openaiShim test extraction seam 112 start: the OpenAI shim façade creates independent client instances -test('the OpenAI shim façade creates independent client instances', () => { - const first = createOpenAIShimClient({}) as OpenAIShimClient - const second = createOpenAIShimClient({}) as OpenAIShimClient - expect(first).not.toBe(second) - expect(first.beta).not.toBe(second.beta) - expect(first.beta.messages).not.toBe(second.beta.messages) -}) -// openaiShim test extraction seam 112 end - -test('raw-text and XML fallback tool calls use one unique sequence', () => { - const text = parseTextToolCalls('{"name":"from_text","arguments":{}}') - const xml = parseXmlToolCalls('{"name":"from_xml","arguments":{}}') - expect(text.calls[0]?.id).toMatch(/^ollama_tc_\d+$/) - expect(xml.calls[0]?.id).toMatch(/^xml_tc_\d+$/) - const textNum = Number(text.calls[0]?.id?.replace(/^\D+/, '')) - const xmlNum = Number(xml.calls[0]?.id?.replace(/^\D+/, '')) - // Same session counter: the second mint must be exactly one greater than the first. - expect(xmlNum).toBe(textNum + 1) -}) - -// --------------------------------------------------------------------------- -// openaiShim test extraction seam 113 start: non-streaming: reasoning_content emitted as thinking block only when content is null -test('non-streaming: reasoning_content emitted as thinking block only when content is null', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5', - choices: [ - { - message: { - role: 'assistant', - content: null, - reasoning_content: 'Let me think about this step by step.', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = (await client.beta.messages.create({ - model: 'glm-5', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - })) as { content: Array> } - - expect(result.content).toEqual([ - { type: 'thinking', thinking: 'Let me think about this step by step.' }, - ]) -}) -// openaiShim test extraction seam 113 end - - -// openaiShim test extraction seam 114 start: non-streaming: empty string content does not fall through to reasoning_content as text -test('non-streaming: empty string content does not fall through to reasoning_content as text', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5', - choices: [ - { - message: { - role: 'assistant', - content: '', - reasoning_content: 'Chain of thought here.', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = (await client.beta.messages.create({ - model: 'glm-5', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - })) as { content: Array> } - - expect(result.content).toEqual([ - { type: 'thinking', thinking: 'Chain of thought here.' }, - ]) -}) -// openaiShim test extraction seam 114 end - - -// openaiShim test extraction seam 115 start: non-streaming: real content takes precedence over reasoning_content -test('non-streaming: real content takes precedence over reasoning_content', async () => { - globalThis.fetch = (async (_input, _init) => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5', - choices: [ - { - message: { - role: 'assistant', - content: 'The answer is 42.', - reasoning_content: 'I need to calculate this.', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = (await client.beta.messages.create({ - model: 'glm-5', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - })) as { content: Array> } - - expect(result.content).toEqual([ - { type: 'thinking', thinking: 'I need to calculate this.' }, - { type: 'text', text: 'The answer is 42.' }, - ]) -}) -// openaiShim test extraction seam 115 end - - -// openaiShim test extraction seam 116 start: non-streaming: preserves response body when usage parsing fails -test('non-streaming: preserves response body when usage parsing fails', async () => { - const json = JSON as unknown as { parse: typeof JSON.parse } - const originalJSONParse = json.parse - const responseBody = JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5', - choices: [ - { - message: { - role: 'assistant', - content: 'ok', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }, - }) - let usageParseFailed = false - - // Throw only for the usage-extraction parse of the response body. - // A global "throw once" mock is unreliable here: Bun's native - // Response.json() does not go through JS-level JSON.parse, so the - // second parse the original test relied on never happens (parseCalls - // stays at 1 and `toBeGreaterThan(1)` fails). Scoping the failure to - // the response body targets the _doRequest parse without breaking - // unrelated JSON.parse calls in the request pipeline, and works in - // both Bun (native Response.json) and Node (undici, which does call - // JSON.parse — guarded by `usageParseFailed` so it won't throw again). - json.parse = ((text: string, reviver?: Parameters[1]) => { - if (!usageParseFailed && text === responseBody) { - usageParseFailed = true - throw new Error('simulated usage parse failure') - } - return originalJSONParse(text, reviver) - }) as typeof JSON.parse - - try { - globalThis.fetch = (async () => { - return new Response(responseBody, { - headers: { - 'Content-Type': 'application/json', - }, - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = (await client.beta.messages.create({ - model: 'glm-5', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - })) as { content: Array> } - - // Usage extraction threw, but the recreated Response still holds the - // body so downstream response.json() can read it. - expect(usageParseFailed).toBe(true) - expect(result.content).toEqual([{ type: 'text', text: 'ok' }]) - } finally { - json.parse = originalJSONParse - } -}) -// openaiShim test extraction seam 116 end - - -// openaiShim test extraction seam 117 start: non-streaming: preserves response.url routing metadata after body read -test('non-streaming: preserves response.url routing metadata after body read', async () => { - // _doRequest reads the body for usage extraction and recreates the - // Response with new Response(bodyText, ...). That drops response.url to - // "", which breaks create()'s /responses, /messages, and Gemini routing. - // This test pins an Anthropic-shaped body behind a /messages URL: if url - // is preserved, create() passes the body through unchanged; if url is - // lost, it falls through to _convertNonStreamingResponse and the - // Anthropic-only fields (stop_reason, input_tokens) surface as wrong - // output or missing content. - const anthropicBody = JSON.stringify({ - id: 'msg_1', - type: 'message', - role: 'assistant', - content: [{ type: 'text', text: 'passthrough ok' }], - model: 'claude-3', - stop_reason: 'end_turn', - usage: { input_tokens: 10, output_tokens: 20 }, - }) - - globalThis.fetch = (async () => { - const r = new Response(anthropicBody, { - headers: { 'Content-Type': 'application/json' }, - }) - // fetch() sets .url from the request; new Response() cannot. Simulate - // the fetch-attached URL so create()'s routing can see /messages. - Object.defineProperty(r, 'url', { - value: 'https://api.anthropic-shaped.example.com/v1/messages', - configurable: true, - }) - return r - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = (await client.beta.messages.create({ - model: 'glm-5', - system: 'test system', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - })) as { content: Array> } - - // /messages passthrough returns the Anthropic body verbatim. If url were - // lost, _convertNonStreamingResponse would try to read OpenAI choices[] - // and content would not match. - expect(result.content).toEqual([{ type: 'text', text: 'passthrough ok' }]) -}) -// openaiShim test extraction seam 117 end - - -// openaiShim test extraction seam 118 start: non-streaming: strips tag block from assistant content -test('non-streaming: strips tag block from assistant content', async () => { - globalThis.fetch = asMockFetch(mock(async () => { - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-5-mini', - choices: [ - { - message: { - role: 'assistant', - content: - 'user wants a greeting, respond brieflyHey! How can I help you today?', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 10, - completion_tokens: 20, - total_tokens: 30, - }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = (await client.beta.messages.create({ - model: 'gpt-5-mini', - system: 'test system', - messages: [{ role: 'user', content: 'hey' }], - max_tokens: 64, - stream: false, - })) as { content: Array> } - - expect(result.content).toEqual([ - { type: 'text', text: 'Hey! How can I help you today?' }, - ]) -}) -// openaiShim test extraction seam 118 end - - -// Extraction seam: non-streaming response conversion | streaming event conversion. - -// openaiShim test extraction seam 119 start: streaming: thinking block closed before tool call -test('streaming: thinking block closed before tool call', async () => { - globalThis.fetch = (async (_input, _init) => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5', - choices: [ - { - index: 0, - delta: { role: 'assistant', reasoning_content: 'Thinking...' }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5', - choices: [ - { - index: 0, - delta: { - tool_calls: [ - { - index: 0, - id: 'call-1', - type: 'function', - function: { - name: 'Bash', - arguments: '{"command":"ls"}', - }, - }, - ], - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'tool_calls', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'glm-5', - system: 'test system', - messages: [{ role: 'user', content: 'Run ls' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) { - events.push(event) - } - - const types = events.map(e => e.type) - - const thinkingStartIdx = types.indexOf('content_block_start') - const firstStopIdx = types.indexOf('content_block_stop') - const toolStartIdx = types.indexOf( - 'content_block_start', - thinkingStartIdx + 1, - ) - - expect(thinkingStartIdx).toBeGreaterThanOrEqual(0) - expect(firstStopIdx).toBeGreaterThan(thinkingStartIdx) - expect(toolStartIdx).toBeGreaterThan(firstStopIdx) - - const thinkingStart = events[thinkingStartIdx] as { - content_block?: Record - } - expect(thinkingStart?.content_block?.type).toBe('thinking') -}) -// openaiShim test extraction seam 119 end - - -// openaiShim test extraction seam 120 start: streaming: strips tag block from assistant content deltas -test('streaming: strips tag block from assistant content deltas', async () => { - globalThis.fetch = asMockFetch(mock(async () => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - content: - 'user wants a greeting, respond brieflyHey! How can I help you today?', - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'stop', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'gpt-5-mini', - system: 'test system', - messages: [{ role: 'user', content: 'hey' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const textDeltas: string[] = [] - for await (const event of result.data) { - const delta = (event as { delta?: { type?: string; text?: string } }).delta - if (delta?.type === 'text_delta' && typeof delta.text === 'string') { - textDeltas.push(delta.text) - } - } - - expect(textDeltas.join('')).toBe('Hey! How can I help you today?') -}) -// openaiShim test extraction seam 120 end - - -// openaiShim test extraction seam 121 start: streaming: strips tag split across multiple content chunks -test('streaming: strips tag split across multiple content chunks', async () => { - globalThis.fetch = asMockFetch(mock(async () => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - content: 'user wants a greeting,', - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: { - content: ' respond brieflyHey! How can I help you today?', - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'stop', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - const result = await client.beta.messages - .create({ - model: 'gpt-5-mini', - system: 'test system', - messages: [{ role: 'user', content: 'hey' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const textDeltas: string[] = [] - for await (const event of result.data) { - const delta = (event as { delta?: { type?: string; text?: string } }).delta - if (delta?.type === 'text_delta' && typeof delta.text === 'string') { - textDeltas.push(delta.text) - } - } - - expect(textDeltas.join('')).toBe('Hey! How can I help you today?') -}) -// openaiShim test extraction seam 121 end - - -// openaiShim test extraction seam 122 start: streaming: preserves prose without tags (no phrase-based false positive) -test('streaming: preserves prose without tags (no phrase-based false positive)', async () => { - // Regression: older phrase-based sanitizer would strip "I should..." prose. - // The tag-based approach leaves legitimate assistant output alone. - globalThis.fetch = asMockFetch(mock(async () => { - const chunks = makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: { - role: 'assistant', - content: - 'I should note that the user role requires a briefly concise friendly response format.', - }, - finish_reason: null, - }, - ], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'gpt-5-mini', - choices: [ - { - index: 0, - delta: {}, - finish_reason: 'stop', - }, - ], - }, - ]) - - return makeSseResponse(chunks) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'gpt-5-mini', - system: 'test system', - messages: [{ role: 'user', content: 'hey' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const textDeltas: string[] = [] - for await (const event of result.data) { - const delta = (event as { delta?: { type?: string; text?: string } }).delta - if (delta?.type === 'text_delta' && typeof delta.text === 'string') { - textDeltas.push(delta.text) - } - } - - expect(textDeltas.join('')).toBe( - 'I should note that the user role requires a briefly concise friendly response format.', - ) -}) -// openaiShim test extraction seam 122 end - - -// Extraction boundary: response conversion | executor network behavior. -// The executor suite owns the contiguous network-classification block below. -// Keep this marker stable for independent adjacent test migrations. -// openaiShim test extraction seam 123 start: strips credentials and query params from URL in fetch network error message -test('strips credentials and query params from URL in fetch network error message', async () => { - process.env.OPENAI_BASE_URL = - 'https://user:password@internal.example.test/v1?token=abc123' - process.env.OPENAI_API_KEY = 'test-key' - - globalThis.fetch = asMockFetch(mock(async () => { - throw new TypeError( - 'fetch failed https://user:password@internal.example.test/v1?token=abc123/chat/completions', - ) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - let caught: unknown - try { - await client.beta.messages.create({ - model: 'test-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - } catch (error) { - caught = error - } - - const message = (caught as Error).message - expect(message).toContain('internal.example.test') - expect(message).toContain('fetch failed') - expect(message).not.toContain('password') - expect(message).not.toContain('user:') - expect(message).not.toContain('token=abc123') -}) -// openaiShim test extraction seam 123 end - -test('redacts configured secret substrings from fetch network error messages', async () => { - const secret = 'route/key+AbC123' - process.env.OPENAI_API_KEY = secret - - globalThis.fetch = asMockFetch(mock(async () => { - throw new TypeError(`fetch failed while routing ${secret}`) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'test-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).rejects.not.toThrow(secret) -}) - -test('redacts encoded configured secrets from non-URL transport error messages', async () => { - const secret = 'route/key+AbC123' - const encodedSecret = encodeURIComponent(secret) - const doubleEncodedSecret = encodeURIComponent(encodedSecret) - const fullyEncodedSecret = Array.from(new TextEncoder().encode(secret)) - .map(byte => `%${byte.toString(16).padStart(2, '0')}`) - .join('') - const malformedAdjacentSecret = `%E0%A4${encodedSecret}` - const encodedCategoryMarker = '%5Bopenai_category%3Dauth_invalid%5D' - const encodedControlSequence = '%1B%5B31m' - process.env.OPENAI_API_KEY = secret - - globalThis.fetch = asMockFetch(mock(async () => { - throw new TypeError( - `proxy failed ${encodedCategoryMarker} ${encodedControlSequence} for path /v1/${encodedSecret}?nested=${doubleEncodedSecret}&fully=${fullyEncodedSecret}&malformed=${malformedAdjacentSecret}`, - ) - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - let caught: unknown - try { - await client.beta.messages.create({ - model: 'test-model', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - } catch (error) { - caught = error - } - - expect(caught).toBeDefined() - const message = (caught as Error).message - expect(message).toContain('proxy failed') - expect(message).toContain(encodedCategoryMarker) - expect(message).toContain(encodedControlSequence) - expect(message).not.toContain('\u001B') - expect(extractOpenAICategoryMarker(message)).toBe('network_error') - expect(message).not.toContain(secret) - expect(message).not.toContain(encodedSecret) - expect(message).not.toContain(doubleEncodedSecret) - expect(message).not.toContain(fullyEncodedSecret) - expect(message).not.toContain(malformedAdjacentSecret) -}) - -// openaiShim test extraction seam 124 start: classifies localhost transport failures with actionable category marker -test('classifies localhost transport failures with actionable category marker', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - - const transportError = Object.assign(new TypeError('fetch failed'), { - code: 'ECONNREFUSED', - }) - - globalThis.fetch = asMockFetch(mock(async () => { - throw transportError - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).rejects.toThrow('openai_category=connection_refused') - - await expect( - client.beta.messages.create({ - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).rejects.toThrow('local server is running') -}) -// openaiShim test extraction seam 124 end - - -// openaiShim test extraction seam 125 start: transport failures are not labeled with HTTP status 503 -test('transport failures are not labeled with HTTP status 503', async () => { - // Issue #971: ENETDOWN (and other transport errors) are emitted before any - // HTTP response is received. Reporting them as "503" makes users believe the - // upstream server returned 503 Service Unavailable. - process.env.OPENAI_BASE_URL = 'https://intranet.example.test/v1' - - const transportError = Object.assign(new TypeError('fetch failed'), { - code: 'ENETDOWN', - }) - - globalThis.fetch = asMockFetch(mock(async () => { - throw transportError - })) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - let caught: unknown - try { - await client.beta.messages.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - } catch (error) { - caught = error - } - - expect(caught).toBeDefined() - const err = caught as { status?: number; message: string; constructor: { name: string } } - expect(err.constructor.name).toBe('APIConnectionError') - expect(err.status).toBeUndefined() - expect(err.message).not.toMatch(/^503\b/) - expect(err.message).toContain('OpenAI API transport error') - expect(err.message).toContain('code=ENETDOWN') - expect(err.message).toContain('openai_category=network_error') -}) -// openaiShim test extraction seam 125 end - -test('propagates caller AbortError without wrapping it as transport failure', async () => { -// openaiShim test extraction seam 126 start: propagates AbortError without wrapping it as transport failure - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - - const abortError = new DOMException('The operation was aborted.', 'AbortError') - globalThis.fetch = asMockFetch(mock(async () => { - throw abortError - })) - - const controller = new AbortController() - const callerReason = new DOMException('Cancelled by caller', 'AbortError') - controller.abort(callerReason) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create( - { - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { signal: controller.signal }, - ), - ).rejects.toBe(callerReason) -}) - -test('classifies a pre-header API timeout without replaying the request', async () => { - process.env.API_TIMEOUT_MS = '20' - const pathSecret = 'route/key+AbC123' - const encodedPathSecret = encodeURIComponent(pathSecret) - const doubleEncodedPathSecret = encodeURIComponent(encodedPathSecret) - const escapeLookingSecret = 'abc%2FdefLONG' - const encodedEscapeLookingSecret = encodeURIComponent(escapeLookingSecret) - const decodedEscapeLookingSecret = decodeURIComponent(escapeLookingSecret) - const malformedUtf8Secret = 'éSECRET_VALUE_123' - const encodedMalformedUtf8Secret = encodeURIComponent(malformedUtf8Secret) - process.env.OPENAI_API_KEY = pathSecret - process.env.OPENROUTER_API_KEY = escapeLookingSecret - process.env.DEEPSEEK_API_KEY = malformedUtf8Secret - process.env.OPENAI_BASE_URL = - `https://user:password@slow.example.test/v1/invalid%ZZ/${doubleEncodedPathSecret}/${encodedEscapeLookingSecret}/%E0%A4${encodedMalformedUtf8Secret}` + - `?prompt=${encodedPathSecret}&nested=${doubleEncodedPathSecret}` + - `&escape=${encodedEscapeLookingSecret}&malformed=%E0%A4${encodedMalformedUtf8Secret}` + - '&token=secret' - let fetchCalls = 0 - let completedGenerations = 0 - const receivedBodies: string[] = [] - globalThis.fetch = (async (_input, init) => { - fetchCalls++ - receivedBodies.push(String(init?.body)) - return new Promise((resolve, reject) => { - setTimeout(() => { - completedGenerations++ - resolve(makeChatCompletionResponse('gpt-4o-mini')) - }, 50) - const signal = init?.signal - if (!signal) return - const rejectFromAbort = () => { - reject(signal.reason ?? new DOMException('Aborted', 'AbortError')) - } - signal.addEventListener('abort', rejectFromAbort, { once: true }) - if (signal.aborted) rejectFromAbort() - }) - }) as unknown as FetchType - - const safety = new AbortController() - const safetyTimer = setTimeout(() => safety.abort(), 500) - const client = createOpenAIShimClient({}) as OpenAIShimClient - - let caught: unknown - try { - await waitForPromise( - client.beta.messages.create( - { - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { signal: safety.signal }, - ), - 750, - 'pre-header timeout did not settle', - ) - } catch (error) { - caught = error - } finally { - clearTimeout(safetyTimer) - } - await new Promise(resolve => setTimeout(resolve, 60)) - - expect(caught).toBeDefined() - const error = caught as Error & { constructor: { name: string } } - expect(error.constructor.name).toBe('APIConnectionError') - expect(isOpenAIRequestNonReplayable(error)).toBe(true) - expect(error.message).toContain('no response headers within 20ms (API_TIMEOUT_MS)') - expect(error.message).toContain('slow.example.test') - expect(error.message).toContain('openai_category=request_timeout') - expect(error.message).not.toContain('password') - expect(error.message).not.toContain('token=secret') - expect(error.message).not.toContain(pathSecret) - expect(error.message).not.toContain(encodedPathSecret) - expect(error.message).not.toContain(doubleEncodedPathSecret) - expect(error.message).not.toContain(escapeLookingSecret) - expect(error.message).not.toContain(encodedEscapeLookingSecret) - expect(error.message).not.toContain(decodedEscapeLookingSecret) - expect(error.message).not.toContain(malformedUtf8Secret) - expect(error.message).not.toContain(encodedMalformedUtf8Secret) - expect(fetchCalls).toBe(1) - expect(receivedBodies).toHaveLength(1) - expect(completedGenerations).toBe(1) -}) - -test('does not proxy-retry when a deadline abort surfaces as fetch failed', async () => { - process.env.API_TIMEOUT_MS = '20' - let fetchCalls = 0 - globalThis.fetch = (async (_input, init) => { - fetchCalls++ - return new Promise((_resolve, reject) => { - const signal = init?.signal - const rejectFromAbort = () => reject(new TypeError('fetch failed')) - signal?.addEventListener('abort', rejectFromAbort, { once: true }) - if (signal?.aborted) rejectFromAbort() - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).rejects.toThrow('no response headers within 20ms (API_TIMEOUT_MS)') - - expect(fetchCalls).toBe(1) -}) - -test('deadline wins when an abort-ignoring fetch resolves 504 afterward', async () => { - process.env.API_TIMEOUT_MS = '20' - let fetchCalls = 0 - globalThis.fetch = (async (_input, init) => { - fetchCalls++ - return new Promise(resolve => { - const resolveAfterAbort = () => { - resolve(new Response('Gateway Timeout', { status: 504 })) - } - init?.signal?.addEventListener('abort', resolveAfterAbort, { once: true }) - if (init?.signal?.aborted) resolveAfterAbort() - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).rejects.toThrow('no response headers within 20ms (API_TIMEOUT_MS)') - - expect(fetchCalls).toBe(1) -}) - -test('gives a proxy retry its own response-header deadline', async () => { - process.env.API_TIMEOUT_MS = '50' - let fetchCalls = 0 - globalThis.fetch = (async () => { - fetchCalls++ - if (fetchCalls === 1) { - await new Promise(resolve => setTimeout(resolve, 30)) - return new Response('Gateway Timeout', { status: 504 }) - } - await new Promise(resolve => setTimeout(resolve, 30)) - return makeChatCompletionResponse('gpt-4o-mini') - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const response = await client.beta.messages.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - expect(response).toBeDefined() - expect(fetchCalls).toBe(2) -}) - -test('bounds nested URL decoding while retaining encoded secret redaction', async () => { - const secret = 'route/key+AbC123' - const secretVariants = [secret] - for (let layer = 0; layer < 4; layer++) { - secretVariants.push( - encodeURIComponent(secretVariants[secretVariants.length - 1]!), - ) - } - const deeplyNestedValue = `%${'25'.repeat(200)}` - process.env.OPENAI_API_KEY = secret - process.env.OPENAI_BASE_URL = - `https://slow.example.test/v1/${deeplyNestedValue}/${secretVariants[4]}` - globalThis.fetch = asMockFetch(mock(async () => { - throw new TypeError('fetch failed') - })) - - const originalDecodeURIComponent = globalThis.decodeURIComponent - let decodeCalls = 0 - globalThis.decodeURIComponent = ((value: string) => { - decodeCalls++ - return originalDecodeURIComponent(value) - }) as typeof decodeURIComponent - - const client = createOpenAIShimClient({}) as OpenAIShimClient - let caught: unknown - try { - await client.beta.messages.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - } catch (error) { - caught = error - } finally { - globalThis.decodeURIComponent = originalDecodeURIComponent - } - - expect(caught).toBeDefined() - for (const secretVariant of secretVariants) { - expect((caught as Error).message).not.toContain(secretVariant) - } - expect(decodeCalls).toBeLessThanOrEqual(32) -}) - -test('decodes malformed URL escape runs in linear work', async () => { - const secret = 'route/key+AbC123' - const encodedSecret = encodeURIComponent(secret) - const malformedEscapeCount = 200 - const malformedUtf8Run = '%80'.repeat(malformedEscapeCount) - process.env.OPENAI_API_KEY = secret - process.env.OPENAI_BASE_URL = - `https://slow.example.test/v1/${malformedUtf8Run}/${encodedSecret}` - globalThis.fetch = asMockFetch(mock(async () => { - throw new TypeError('fetch failed') - })) - - const originalDecodeURIComponent = globalThis.decodeURIComponent - let malformedDecodeCalls = 0 - globalThis.decodeURIComponent = ((value: string) => { - if (value.includes('%80')) malformedDecodeCalls++ - return originalDecodeURIComponent(value) - }) as typeof decodeURIComponent - - const client = createOpenAIShimClient({}) as OpenAIShimClient - let caught: unknown - try { - await client.beta.messages.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - } catch (error) { - caught = error - } finally { - globalThis.decodeURIComponent = originalDecodeURIComponent - } - - expect(caught).toBeDefined() - expect((caught as Error).message).not.toContain(secret) - expect((caught as Error).message).not.toContain(encodedSecret) - expect(malformedDecodeCalls).toBeLessThanOrEqual(malformedEscapeCount * 10) -}) - -test('preserves caller cancellation while waiting for response headers without retrying', async () => { - process.env.API_TIMEOUT_MS = '200' - let fetchCalls = 0 - globalThis.fetch = (async (_input, init) => { - fetchCalls++ - return pendingFetchUntilAbort(init) - }) as unknown as FetchType - - const caller = new AbortController() - const callerReason = new DOMException('Cancelled by user', 'AbortError') - const client = createOpenAIShimClient({}) as OpenAIShimClient - const originalAbortSignalAny = Object.getOwnPropertyDescriptor( - AbortSignal, - 'any', - ) - Object.defineProperty(AbortSignal, 'any', { - value: undefined, - configurable: true, - }) - try { - const request = client.beta.messages.create( - { - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { signal: caller.signal }, - ) - - setTimeout(() => caller.abort(callerReason), 10) - - await expect( - waitForPromise(request, 500, 'caller abort did not settle'), - ).rejects.toBe(callerReason) - expect(fetchCalls).toBe(1) - } finally { - if (originalAbortSignalAny) { - Object.defineProperty(AbortSignal, 'any', originalAbortSignalAny) - } - } -}) - -test('native signal composition preserves the caller abort reason when fetch rejects AbortError', async () => { - expect(typeof AbortSignal.any).toBe('function') - process.env.API_TIMEOUT_MS = '200' - let fetchCalls = 0 - const fetchAbortError = new DOMException( - 'The operation was aborted.', - 'AbortError', - ) - globalThis.fetch = (async (_input, init) => { - fetchCalls++ - return new Promise((_resolve, reject) => { - const signal = init?.signal - if (!signal) return - const rejectFromAbort = () => reject(fetchAbortError) - signal.addEventListener('abort', rejectFromAbort, { once: true }) - if (signal.aborted) rejectFromAbort() - }) - }) as unknown as FetchType - - const caller = new AbortController() - const callerReason = new DOMException('Cancelled by user', 'AbortError') - const client = createOpenAIShimClient({}) as OpenAIShimClient - const request = client.beta.messages.create( - { - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { signal: caller.signal }, - ) - - const abortTimer = setTimeout(() => caller.abort(callerReason), 10) - - try { - await expect( - waitForPromise(request, 500, 'caller abort did not settle'), - ).rejects.toBe(callerReason) - } finally { - clearTimeout(abortTimer) - } - expect(fetchCalls).toBe(1) -}) - -test('caller abort winning the timeout catch race prevents a retry', async () => { - process.env.API_TIMEOUT_MS = '20' - let fetchCalls = 0 - const caller = new AbortController() - const callerReason = new DOMException('Cancelled by user', 'AbortError') - globalThis.fetch = (async (_input, init) => { - fetchCalls++ - return new Promise((_resolve, reject) => { - const signal = init?.signal - if (!signal) return - const rejectFromAbort = () => { - reject(signal.reason ?? new DOMException('Aborted', 'AbortError')) - if (fetchCalls === 1) { - queueMicrotask(() => caller.abort(callerReason)) - } - } - signal.addEventListener('abort', rejectFromAbort, { once: true }) - if (signal.aborted) rejectFromAbort() - }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - waitForPromise( - client.beta.messages.create( - { - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { signal: caller.signal }, - ), - 500, - 'caller abort race did not settle', - ), - ).rejects.toBe(callerReason) - expect(fetchCalls).toBe(1) -}) - -test('manual signal fallback preserves caller cancellation after headers arrive', async () => { - process.env.API_TIMEOUT_MS = '200' - const fetchSignals: AbortSignal[] = [] - const stalled = makeStallingResponse( - makeOpenAIStreamFrame({ role: 'assistant', content: 'started' }), - ) - globalThis.fetch = (async (_input, init) => { - if (init?.signal) fetchSignals.push(init.signal) - return stalled.response - }) as unknown as FetchType - - const caller = new AbortController() - const client = createOpenAIShimClient({}) as OpenAIShimClient - const originalAbortSignalAny = Object.getOwnPropertyDescriptor( - AbortSignal, - 'any', - ) - Object.defineProperty(AbortSignal, 'any', { - value: undefined, - configurable: true, - }) - try { - const result = await client.beta.messages - .create( - { - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }, - { signal: caller.signal }, - ) - .withResponse() - - await expectAbortStopsStream({ - abort: () => caller.abort(), - cancelReasons: stalled.cancelReasons, - expectedEventsBeforeAbort: 1, - label: 'manual combined signal after headers', - stream: result.data as ShimStream, - }) - - expect(fetchSignals).toHaveLength(1) - expect(fetchSignals[0].aborted).toBe(true) - } finally { - stalled.close() - if (originalAbortSignalAny) { - Object.defineProperty(AbortSignal, 'any', originalAbortSignalAny) - } - } -}) - -test('manual signal fallback removes caller forwarding after the body settles', async () => { - process.env.API_TIMEOUT_MS = '200' - globalThis.fetch = asMockFetch(mock(async () => - makeChatCompletionResponse('gpt-4o-mini'))) - - const caller = new AbortController() - const client = createOpenAIShimClient({}) as OpenAIShimClient - const originalAbortSignalAny = Object.getOwnPropertyDescriptor( - AbortSignal, - 'any', - ) - Object.defineProperty(AbortSignal, 'any', { - value: undefined, - configurable: true, - }) - try { - for (let request = 0; request < 2; request++) { - await client.beta.messages.create( - { - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }, - { signal: caller.signal }, - ) - expect(getEventListeners(caller.signal, 'abort')).toHaveLength(0) - } - } finally { - if (originalAbortSignalAny) { - Object.defineProperty(AbortSignal, 'any', originalAbortSignalAny) - } - } -}) - -test('disarms the API timeout after headers arrive while the body keeps streaming', async () => { - process.env.API_TIMEOUT_MS = '20' - const fetchSignals: AbortSignal[] = [] - const encoder = new TextEncoder() - globalThis.fetch = (async (_input, init) => { - if (init?.signal) fetchSignals.push(init.signal) - return new Response( - new ReadableStream({ - start(controller) { - setTimeout(() => { - controller.enqueue(encoder.encode(makeOpenAIStreamFrame( - { role: 'assistant', content: 'late body' }, - 'stop', - ))) - controller.enqueue(encoder.encode('data: [DONE]\n\n')) - controller.close() - }, 50) - }, - }), - { headers: { 'Content-Type': 'text/event-stream' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - const events: Array> = [] - for await (const event of result.data) events.push(event) - - expect(events.length).toBeGreaterThan(0) - expect(fetchSignals).toHaveLength(1) - expect(fetchSignals[0].aborted).toBe(false) -}) -// openaiShim test extraction seam 126 end - - -// openaiShim test extraction seam 127 start: classifies chat-completions endpoint 404 failures with endpoint_not_found marker -test('classifies chat-completions endpoint 404 failures with endpoint_not_found marker', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434' - - globalThis.fetch = asMockFetch(mock(async () => - new Response('Not Found', { - status: 404, - headers: { - 'Content-Type': 'text/plain', - }, - }))) - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).rejects.toThrow('openai_category=endpoint_not_found') -}) -// openaiShim test extraction seam 127 end - -// openaiShim test extraction seam 128 start: self-heals localhost resolution failures by retrying local loopback base URL -test('self-heals localhost resolution failures by retrying local loopback base URL', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - - const requestUrls: string[] = [] - globalThis.fetch = (async (input, _init) => { - const url = typeof input === 'string' ? input : input.url - requestUrls.push(url) - - if (url.includes('localhost')) { - const error = Object.assign(new TypeError('fetch failed'), { - code: 'ENOTFOUND', - }) - throw error - } - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'qwen2.5-coder:7b', - choices: [ - { - message: { - role: 'assistant', - content: 'hello from loopback', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 4, - completion_tokens: 3, - total_tokens: 7, - }, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).resolves.toBeDefined() - - expect(requestUrls[0]).toBe('http://localhost:11434/api/chat') - expect(requestUrls).toContain('http://127.0.0.1:11434/api/chat') -}) -// openaiShim test extraction seam 128 end - - -// Extraction boundary: executor network behavior | native Ollama routing. -// Native Ollama endpoint selection remains an adapter/facade integration concern. -// Keep this marker stable for independent adjacent test migrations. -// openaiShim test extraction seam 129 start: uses native Ollama chat endpoint when local base URL omits /v1 -test('uses native Ollama chat endpoint when local base URL omits /v1', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434' - - const requestUrls: string[] = [] - globalThis.fetch = (async (input, _init) => { - const url = typeof input === 'string' ? input : input.url - requestUrls.push(url) - - return new Response( - JSON.stringify({ - model: 'qwen2.5-coder:7b', - message: { - role: 'assistant', - content: 'hello from native Ollama', - }, - done: true, - done_reason: 'stop', - prompt_eval_count: 5, - eval_count: 2, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).resolves.toBeDefined() - - expect(requestUrls).toEqual(['http://localhost:11434/api/chat']) -}) -// openaiShim test extraction seam 129 end - - -// openaiShim test extraction seam 130 start: keeps remote Ollama-named gateways on chat completions -test('keeps remote Ollama-named gateways on chat completions', async () => { - process.env.OPENAI_BASE_URL = 'https://ollama-gateway.example.com/v1' - - const requestUrls: string[] = [] - globalThis.fetch = (async (input, init) => { - const url = typeof input === 'string' ? input : input.url - requestUrls.push(url) - const body = JSON.parse(String(init?.body)) as Record - expect(body.max_tokens).toBe(64) - expect(body.options).toBeUndefined() - - return makeChatCompletionResponse('llama3.1:8b') - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'llama3.1:8b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).resolves.toBeDefined() - - expect(requestUrls).toEqual([ - 'https://ollama-gateway.example.com/v1/chat/completions', - ]) -}) -// openaiShim test extraction seam 130 end - - -// openaiShim test extraction seam 131 start: keeps HTTPS localhost Ollama-port proxies on chat completions -test('keeps HTTPS localhost Ollama-port proxies on chat completions', async () => { - process.env.OPENAI_BASE_URL = 'https://localhost:11434/v1' - - const requestUrls: string[] = [] - globalThis.fetch = (async (input, init) => { - const url = typeof input === 'string' ? input : input.url - requestUrls.push(url) - const body = JSON.parse(String(init?.body)) as Record - expect(body.max_tokens).toBe(64) - expect(body.options).toBeUndefined() - - return makeChatCompletionResponse('llama3.1:8b') - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'llama3.1:8b', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }), - ).resolves.toBeDefined() - - expect(requestUrls).toEqual([ - 'https://localhost:11434/v1/chat/completions', - ]) -}) -// openaiShim test extraction seam 131 end - - -// Extraction boundary: native Ollama routing | executor tool self-healing. -// The single retry test below moves with request execution. -// Keep this marker stable for independent adjacent test migrations. -// openaiShim test extraction seam 132 start: self-heals tool-call incompatibility by retrying local Ollama requests without tools -test('self-heals tool-call incompatibility by retrying local Ollama requests without tools', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' - - const requestBodies: Array> = [] - globalThis.fetch = (async (_input, init) => { - const requestBody = JSON.parse(String(init?.body)) as Record - requestBodies.push(requestBody) - - if (requestBodies.length === 1) { - return new Response('tool_calls are not supported', { - status: 400, - headers: { - 'Content-Type': 'text/plain', - }, - }) - } - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'qwen2.5-coder:7b', - choices: [ - { - message: { - role: 'assistant', - content: 'fallback without tools', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 8, - completion_tokens: 4, - total_tokens: 12, - }, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await expect( - client.beta.messages.create({ - model: 'qwen2.5-coder:7b', - messages: [{ role: 'user', content: 'hello' }], - tools: [ - { - name: 'Read', - description: 'Read a file', - input_schema: { - type: 'object', - properties: { - filePath: { type: 'string' }, - }, - required: ['filePath'], - }, - }, - ], - max_tokens: 64, - stream: false, - }), - ).resolves.toBeDefined() - - expect(requestBodies).toHaveLength(2) - expect(Array.isArray(requestBodies[0]?.tools)).toBe(true) - expect(requestBodies[0]?.tool_choice).toBeUndefined() - expect( - requestBodies[1]?.tools === undefined || - (Array.isArray(requestBodies[1]?.tools) && requestBodies[1]?.tools.length === 0), - ).toBe(true) - expect(requestBodies[1]?.tool_choice).toBeUndefined() - expect(requestBodies[1]?.tool_stream).toBeUndefined() -}) -// openaiShim test extraction seam 132 end - - -// Extraction boundary: executor tool self-healing | message conversion. -// Message-history normalization below belongs to the message converter. -// Keep this marker stable for independent adjacent test migrations. -// openaiShim test extraction seam 133 start: preserves valid tool_result and drops orphan tool_result -test('preserves valid tool_result and drops orphan tool_result', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'mistral-large-latest', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mistral-large-latest', - system: 'test system', - messages: [ - { role: 'user', content: 'Search and then I will interrupt' }, - { - role: 'assistant', - content: [ - { - type: 'tool_use', - id: 'valid_call_1', - name: 'Search', - input: { query: 'openclaude' }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'valid_call_1', - content: 'Found it!', - }, - { - type: 'tool_result', - tool_use_id: 'orphan_call_2', - content: 'Interrupted result', - }, - { - role: 'user', - content: 'What happened?', - }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - - // Should have: system, user, assistant (tool_use), tool (valid_call_1), user - // Should NOT have: tool (orphan_call_2) - - const toolMessages = messages.filter(m => m.role === 'tool') - expect(toolMessages.length).toBe(1) - expect(toolMessages[0].tool_call_id).toBe('valid_call_1') - - const orphanMessage = toolMessages.find(m => m.tool_call_id === 'orphan_call_2') - expect(orphanMessage).toBeUndefined() - // Actually, the semantic message IS injected here because the user block with orphan - // tool result is converted to: - // 1. Tool result (valid_call_1) -> role 'tool' - // 2. User content ("What happened?") -> role 'user' - // This triggers the tool -> assistant injection. - const assistantMessages = messages.filter(m => m.role === 'assistant') - expect(assistantMessages.some(m => m.content === '[Tool results received]')).toBe(true) -}) -// openaiShim test extraction seam 133 end - - -// openaiShim test extraction seam 134 start: drops empty assistant message when only thinking block was present and stripped -test('drops empty assistant message when only thinking block was present and stripped', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response(JSON.stringify({ - id: 'chatcmpl-1', - object: 'chat.completion', - created: 123456789, - model: 'mistral-large-latest', - choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } - }), { headers: { 'Content-Type': 'application/json' } }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mistral-large-latest', - messages: [ - { role: 'user', content: 'Initial' }, - { role: 'assistant', content: [{ type: 'thinking', thinking: 'I am thinking...', signature: 'sig' }] }, - { role: 'user', content: 'Interrupting query' }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - // The assistant msg is dropped because thinking is stripped. - // The two user messages are coalesced. - expect(messages.length).toBe(1) - expect(messages[0].role).toBe('user') - expect(String(messages[0].content)).toContain('Initial') - expect(String(messages[0].content)).toContain('Interrupting query') -}) -// openaiShim test extraction seam 134 end - - -// openaiShim test extraction seam 135 start: drops empty assistant message when only redacted_thinking block was present and stripped -test('drops empty assistant message when only redacted_thinking block was present and stripped', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response(JSON.stringify({ - id: 'chatcmpl-1', - object: 'chat.completion', - created: 123456789, - model: 'mistral-large-latest', - choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } - }), { headers: { 'Content-Type': 'application/json' } }) - }) as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mistral-large-latest', - messages: [ - { role: 'user', content: 'Initial' }, - { role: 'assistant', content: [{ type: 'redacted_thinking', data: '[thinking hidden]' }] }, - { role: 'user', content: 'Interrupting query' }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - // The assistant msg is dropped because redacted_thinking is stripped. - // The two user messages are coalesced. - expect(messages.length).toBe(1) - expect(messages[0].role).toBe('user') - expect(String(messages[0].content)).toContain('Initial') - expect(String(messages[0].content)).toContain('Interrupting query') -}) -// openaiShim test extraction seam 135 end - - -// openaiShim test extraction seam 136 start: injects semantic assistant message when tool result is followed by user message -test('injects semantic assistant message when tool result is followed by user message', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response(JSON.stringify({ - id: 'chatcmpl-2', - object: 'chat.completion', - created: 123456789, - model: 'mistral-large-latest', - choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } - }), { headers: { 'Content-Type': 'application/json' } }) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'mistral-large-latest', - messages: [ - { - role: 'assistant', - content: [{ type: 'tool_use', id: 'call_1', name: 'search', input: {} }] - }, - { - role: 'user', - content: [ - { type: 'tool_result', tool_use_id: 'call_1', content: 'Result' } - ] - }, - { role: 'user', content: 'Next user query' }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - // Roles should be: assistant (tool_calls) -> tool -> assistant (semantic) -> user - const roles = messages.map(m => m.role) - expect(roles).toEqual(['assistant', 'tool', 'assistant', 'user']) - - const semanticMsg = messages[2] - expect(semanticMsg.role).toBe('assistant') - expect(semanticMsg.content).toBe('[Tool results received]') - expect(semanticMsg.content).not.toContain('interrupted') - expect(semanticMsg.content).not.toContain('user') -}) -// openaiShim test extraction seam 136 end - - -// Extraction boundary: executor tool self-healing | message/provider shaping. -// Provider request shaping below is not owned by the executor. -// Keep this marker stable for independent adjacent test migrations. -// openaiShim test extraction seam 137 start: Moonshot: uses max_tokens (not max_completion_tokens) and strips store -test('Moonshot: uses max_tokens (not max_completion_tokens) and strips store', async () => { - process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' - process.env.OPENAI_API_KEY = 'sk-moonshot-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'kimi-k2.6', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'kimi-k2.6', - system: 'you are kimi', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 256, - stream: false, - }) - - expect(requestBody?.max_tokens).toBe(256) - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 137 end - - -// openaiShim test extraction seam 138 start: Cerebras: strips unsupported store on chat_completions (#1023) -test('Cerebras: strips unsupported store on chat_completions (#1023)', async () => { - process.env.OPENAI_BASE_URL = 'https://api.cerebras.ai/v1' - process.env.OPENAI_API_KEY = 'csk-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'llama3.1-8b', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'llama3.1-8b', - system: 'you are cerebras', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 138 end - - -// openaiShim test extraction seam 139 start: Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_completions (#672) -test('Local provider (vLLM/Ollama/etc.): strips unsupported store on chat_completions (#672)', async () => { - process.env.OPENAI_BASE_URL = 'http://localhost:8000/v1' - process.env.OPENAI_API_KEY = 'sk-local' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'qwen-3.5-27b', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'qwen-3.5-27b', - system: 'you are local', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 139 end - - -// openaiShim test extraction seam 140 start: Mistral: strips unsupported store on chat_completions (#739) -test('Mistral: strips unsupported store on chat_completions (#739)', async () => { - process.env.OPENAI_BASE_URL = 'https://api.mistral.ai/v1' - process.env.OPENAI_API_KEY = 'mistral-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'codestral-2508', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'codestral-2508', - system: 'you are mistral', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 140 end - - -// openaiShim test extraction seam 141 start: Mistral host fallback: strips store on an unresolved Mistral-host route (#739) -test('Mistral host fallback: strips store on an unresolved Mistral-host route (#739)', async () => { - // `api.mistral.ai/v1` resolves to the Mistral descriptor route, whose - // removeBodyFields already strips `store` — so the test above passes even - // without the hasMistralApiHost fallback. This case pins the fallback's real - // value: a Mistral-host proxy (`proxy.mistral.ai`) that does NOT resolve to a - // descriptor route (resolveRouteIdFromBaseUrl returns null, no - // removeBodyFields), so `store` is stripped *only* by hasMistralApiHost. - process.env.OPENAI_BASE_URL = 'https://proxy.mistral.ai/v1' - process.env.OPENAI_API_KEY = 'mistral-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'codestral-2508', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'codestral-2508', - system: 'you are mistral', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - // The shim sets `store: false` on every chat_completions body; without the - // fallback this unresolved route would forward it and hit Mistral's 422. - expect(requestBody?.store).toBeUndefined() - // #739's Mistral 422 rejects `max_completion_tokens` as well — the host - // fallback must also map it to `max_tokens` on the unresolved route, since - // the generic config leaves the `max_completion_tokens` default. - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.max_tokens).toBe(64) -}) -// openaiShim test extraction seam 141 end - - -// openaiShim test extraction seam 142 start: hasMistralApiHost matches the Mistral host and its subdomains only - -// openaiShim test extraction seam 142 end - - -// openaiShim test extraction seam 143 start: Groq: keeps max_completion_tokens and strips unsupported store -test('Groq: keeps max_completion_tokens and strips unsupported store', async () => { - process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' - process.env.OPENAI_API_KEY = 'gsk-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'llama-3.3-70b-versatile', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'llama-3.3-70b-versatile', - system: 'you are groq', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 256, - stream: false, - }) - - expect(requestBody?.max_completion_tokens).toBe(256) - expect(requestBody?.max_tokens).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 143 end - - - -// openaiShim test extraction seam 144 start: Groq: strips reasoning_effort even when compat inference matches the model -test('Groq: strips reasoning_effort even when compat inference matches the model', async () => { - process.env.OPENAI_BASE_URL = 'https://api.groq.com/openai/v1' - process.env.OPENAI_API_KEY = 'gsk-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-r1-distill-llama-70b', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ reasoningEffort: 'xhigh' }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-r1-distill-llama-70b', - system: 'you are groq', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 256, - stream: false, - thinking: { type: 'enabled' }, - }) - - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 144 end - -// openaiShim test extraction seam 145 start: Moonshot: echoes reasoning_content on assistant tool-call messages -test('Moonshot: echoes reasoning_content on assistant tool-call messages', async () => { - // Regression for: "API Error: 400 {"error":{"message":"thinking is enabled - // but reasoning_content is missing in assistant tool call message at index - // N"}}" when the agent sends a prior-turn assistant response back to Kimi. - // The thinking block captured from the inbound response must round-trip - // as reasoning_content on the outgoing echoed assistant message. - process.env.OPENAI_BASE_URL = 'https://api.moonshot.ai/v1' - process.env.OPENAI_API_KEY = 'sk-moonshot-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'kimi-k2.6', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'kimi-k2.6', - system: 'you are kimi', - messages: [ - { role: 'user', content: 'check the logs' }, - { - role: 'assistant', - content: [ - { - type: 'thinking', - thinking: 'Need to inspect logs via Bash; running a cat.', - }, - { type: 'text', text: "I'll inspect the logs." }, - { - type: 'tool_use', - id: 'call_bash_1', - name: 'Bash', - input: { command: 'cat /tmp/app.log' }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_bash_1', - content: 'log line 1\nlog line 2', - }, - ], - }, - ], - max_tokens: 256, - stream: false, - }) - - const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - m => m.role === 'assistant' && Array.isArray(m.tool_calls), - ) - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to inspect logs via Bash; running a cat.', - ) -}) -// openaiShim test extraction seam 145 end - - -// openaiShim test extraction seam 146 start: DeepSeek echoes reasoning_content on assistant tool-call messages -test('DeepSeek echoes reasoning_content on assistant tool-call messages', async () => { - process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' - process.env.OPENAI_API_KEY = 'sk-deepseek' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-v4-flash', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-v4-flash', - system: 'test', - messages: [ - { role: 'user', content: 'hi' }, - { - role: 'assistant', - content: [ - { type: 'thinking', thinking: 'thought' }, - { type: 'text', text: 'hello' }, - { - type: 'tool_use', - id: 'call_1', - name: 'Bash', - input: { command: 'ls' }, - }, - ], - }, - { - role: 'user', - content: [ - { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, - ], - }, - ], - max_tokens: 32, - stream: false, - }) - - const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - m => m.role === 'assistant' && Array.isArray(m.tool_calls), - ) - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe('thought') -}) -// openaiShim test extraction seam 146 end - - -// openaiShim test extraction seam 147 start: generic OpenAI-compatible providers do not echo reasoning_content on assistant tool-call messages -test('generic OpenAI-compatible providers do not echo reasoning_content on assistant tool-call messages', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_API_KEY = 'sk-openai-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-4o', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'gpt-4o', - system: 'test', - messages: [ - { role: 'user', content: 'hi' }, - { - role: 'assistant', - content: [ - { type: 'thinking', thinking: 'thought' }, - { type: 'text', text: 'hello' }, - { - type: 'tool_use', - id: 'call_1', - name: 'Bash', - input: { command: 'ls' }, - }, - ], - }, - { - role: 'user', - content: [ - { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, - ], - }, - ], - max_tokens: 32, - stream: false, - }) - - const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - m => m.role === 'assistant' && Array.isArray(m.tool_calls), - ) - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBeUndefined() -}) -// openaiShim test extraction seam 147 end - - -// openaiShim test extraction seam 148 start: gateway-routed DeepSeek models inherit descriptor-backed reasoning and token shaping -test('gateway-routed DeepSeek models inherit descriptor-backed reasoning and token shaping', async () => { - process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.OPENAI_BASE_URL = 'https://openrouter.ai/api/v1' - process.env.OPENAI_API_KEY = 'sk-openrouter-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek/deepseek-reasoner', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek/deepseek-reasoner', - system: 'test', - messages: [ - { role: 'user', content: 'hi' }, - { - role: 'assistant', - content: [ - { type: 'thinking', thinking: 'thought' }, - { type: 'text', text: 'hello' }, - { - type: 'tool_use', - id: 'call_1', - name: 'Bash', - input: { command: 'ls' }, - }, - ], - }, - { - role: 'user', - content: [ - { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, - ], - }, - ], - max_tokens: 64, - stream: false, - thinking: { type: 'enabled' }, - }) - - const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - message => message.role === 'assistant' && Array.isArray(message.tool_calls), - ) - - expect(assistantWithToolCall?.reasoning_content).toBe('thought') - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('max') - expect(requestBody?.max_tokens).toBe(64) - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 148 end - - -// openaiShim test extraction seam 149 start: Moonshot: cn host is also detected -test('Moonshot: cn host is also detected', async () => { - process.env.OPENAI_BASE_URL = 'https://api.moonshot.cn/v1' - process.env.OPENAI_API_KEY = 'sk-moonshot-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'kimi-k2.6', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'kimi-k2.6', - system: 'you are kimi', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 256, - stream: false, - }) - - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 149 end - - -// openaiShim test extraction seam 150 start: Kimi Code endpoint inherits Moonshot max_tokens/store compatibility -test('Kimi Code endpoint inherits Moonshot max_tokens/store compatibility', async () => { - process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1' - process.env.OPENAI_API_KEY = 'sk-kimi-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'kimi-for-coding', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'kimi-for-coding', - system: 'you are kimi code', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 256, - stream: false, - }) - - expect(requestBody?.max_tokens).toBe(256) - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 150 end - - -// openaiShim test extraction seam 151 start: Kimi Code endpoint echoes reasoning_content on assistant tool-call messages -test('Kimi Code endpoint echoes reasoning_content on assistant tool-call messages', async () => { - process.env.OPENAI_BASE_URL = 'https://api.kimi.com/coding/v1' - process.env.OPENAI_API_KEY = 'sk-kimi-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'kimi-for-coding', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'kimi-for-coding', - system: 'you are kimi code', - messages: [ - { role: 'user', content: 'check the logs' }, - { - role: 'assistant', - content: [ - { - type: 'thinking', - thinking: 'Need to inspect logs via Bash; running a cat.', - }, - { type: 'text', text: "I'll inspect the logs." }, - { - type: 'tool_use', - id: 'call_bash_1', - name: 'Bash', - input: { command: 'cat /tmp/app.log' }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_bash_1', - content: 'log line 1\nlog line 2', - }, - ], - }, - ], - max_tokens: 256, - stream: false, - }) - - const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - m => m.role === 'assistant' && Array.isArray(m.tool_calls), - ) - expect(assistantWithToolCall).toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to inspect logs via Bash; running a cat.', - ) -}) -// openaiShim test extraction seam 151 end - - -// openaiShim test extraction seam 152 start: DeepSeek sends thinking toggle and normalized reasoning effort -test('DeepSeek sends thinking toggle and normalized reasoning effort', async () => { - process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' - process.env.OPENAI_API_KEY = 'sk-deepseek' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-v4-pro', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-v4-pro', - system: 'test', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - thinking: { type: 'enabled' }, - }) - - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('max') - expect(requestBody?.max_tokens).toBe(64) - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 152 end - - -// openaiShim test extraction seam 153 start: NVIDIA NIM DeepSeek sends chat template thinking kwargs - -// openaiShim test extraction seam 153 end - - -// openaiShim test extraction seam 154 start: NVIDIA NIM DeepSeek omits chat template thinking kwargs when thinking is disabled - -// openaiShim test extraction seam 154 end - - -// openaiShim test extraction seam 155 start: DeepSeek omits thinking controls when the Anthropic-side request does not set them -test('DeepSeek omits thinking controls when the Anthropic-side request does not set them', async () => { - process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' - process.env.OPENAI_API_KEY = 'sk-deepseek' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-v4-flash', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-v4-flash', - system: 'test', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 32, - stream: false, - }) - - expect(requestBody?.thinking).toBeUndefined() - expect(requestBody?.reasoning_effort).toBeUndefined() -}) -// openaiShim test extraction seam 155 end - - -// openaiShim test extraction seam 156 start: DeepSeek forwards an explicit thinking disable toggle for V4 models -test('DeepSeek forwards an explicit thinking disable toggle for V4 models', async () => { - process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' - process.env.OPENAI_API_KEY = 'sk-deepseek' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-v4-flash', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'deepseek-v4-flash', - system: 'test', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 32, - stream: false, - thinking: { type: 'disabled' }, - }) - - expect(requestBody?.thinking).toEqual({ type: 'disabled' }) - expect(requestBody?.reasoning_effort).toBeUndefined() -}) -// openaiShim test extraction seam 156 end - - - -// openaiShim test extraction seam 157 start: collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774) -test('collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774)', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-reasoner', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'deepseek-reasoner', - system: 'test system', - messages: [ - { role: 'user', content: 'Run ls' }, - { - role: 'assistant', - content: [ - { - type: 'tool_use', - id: 'call_1', - name: 'Bash', - input: { command: 'ls' }, - }, - ], - }, - { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_1', - content: [ - { type: 'text', text: 'line one' }, - { type: 'text', text: 'line two' }, - ], - }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - const toolMessages = messages.filter(m => m.role === 'tool') - expect(toolMessages.length).toBe(1) - expect(toolMessages[0].tool_call_id).toBe('call_1') - expect(typeof toolMessages[0].content).toBe('string') - expect(toolMessages[0].content).toBe('line one\n\nline two') -}) -// openaiShim test extraction seam 157 end - - -// openaiShim test extraction seam 158 start: collapses multiple text blocks into a single string for DeepSeek compatibility (issue #774) -test('collapses multiple text blocks into a single string for DeepSeek compatibility (issue #774)', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'deepseek-reasoner', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'deepseek-reasoner', - system: 'test system', - messages: [ - { - role: 'user', - content: [ - { type: 'text', text: 'Hello!' }, - { type: 'text', text: 'How are you?' }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - expect(messages.length).toBe(2) // system + user - expect(messages[1].role).toBe('user') - expect(typeof messages[1].content).toBe('string') - expect(messages[1].content).toBe('Hello!\n\nHow are you?') -}) -// openaiShim test extraction seam 158 end - - -// openaiShim test extraction seam 159 start: preserves mixed text and image tool results as multipart content -test('preserves mixed text and image tool results as multipart content', async () => { - let requestBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-4o', - choices: [ - { - message: { - role: 'assistant', - content: 'done', - }, - finish_reason: 'stop', - }, - ], - usage: { - prompt_tokens: 12, - completion_tokens: 4, - total_tokens: 16, - }, - }), - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'gpt-4o', - system: 'test system', - messages: [ - { role: 'user', content: 'Show me' }, - { - role: 'assistant', - content: [ - { - type: 'tool_use', - id: 'call_1', - name: 'Bash', - input: { command: 'cat image.png' }, - }, - ], - }, + await expect( + client.beta.messages.create( { - role: 'user', - content: [ - { - type: 'tool_result', - tool_use_id: 'call_1', - content: [ - { type: 'text', text: 'Here is the image:' }, - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: 'iVBORw0KGgo=', - }, - }, - ], - }, - ], + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, }, - ], - max_tokens: 64, - stream: false, - }) - - const messages = requestBody?.messages as Array> - const toolMessages = messages.filter(m => m.role === 'tool') - expect(toolMessages.length).toBe(1) - expect(Array.isArray(toolMessages[0].content)).toBe(true) - const content = toolMessages[0].content as Array> - expect(content.length).toBe(2) - expect(content[0].type).toBe('text') - expect(content[1].type).toBe('image_url') -}) -// openaiShim test extraction seam 159 end - - -// openaiShim test extraction seam 160 start: Z.AI: uses max_tokens (not max_completion_tokens) and strips store -test('Z.AI: uses max_tokens (not max_completion_tokens) and strips store', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'GLM-5.1', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'GLM-5.1', - system: 'you are glm', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 256, - stream: false, - }) - - expect(requestBody?.max_tokens).toBe(256) - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.store).toBeUndefined() -}) -// openaiShim test extraction seam 160 end - - -// openaiShim test extraction seam 161 start: Z.AI: thinking mode enabled when requested -test('Z.AI: thinking mode enabled when requested', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'GLM-5.1', - choices: [ - { - message: { - role: 'assistant', - content: null, - reasoning_content: 'Let me think...', - }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'GLM-5.1', - system: 'you are glm', - messages: [{ role: 'user', content: 'think hard' }], - max_tokens: 1024, - stream: false, - thinking: { type: 'enabled', budget_tokens: 1024 }, - }) - - expect((requestBody?.thinking as Record)?.type).toBe('enabled') - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.max_tokens).toBe(1024) -}) -// openaiShim test extraction seam 161 end - - -// openaiShim test extraction seam 162 start: Z.AI GLM-5.2: default request relies on provider thinking defaults -test('Z.AI GLM-5.2: default request relies on provider thinking defaults', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.model).toBe('glm-5.2') - expect(requestBody?.thinking).toBeUndefined() - expect(requestBody?.reasoning_effort).toBeUndefined() -}) -// openaiShim test extraction seam 162 end - - -// openaiShim test extraction seam 163 start: Z.AI GLM-5.2: user-selected xhigh effort maps to provider max effort -test('Z.AI GLM-5.2: user-selected xhigh effort maps to provider max effort', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.model).toBe('glm-5.2') - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('max') + { signal: controller.signal }, + ), + ).rejects.toBe(callerReason) }) -// openaiShim test extraction seam 163 end - - -test.each([ - ['glm-5.2?reasoning=low', 'high'], - ['glm-5.2?reasoning=medium', 'high'], - ['glm-5.2?reasoning=high', 'high'], - ['glm-5.2?reasoning=xhigh', 'max'], - ['openrouter/zhipu/glm-5.2?reasoning=low', 'high'], - ['openrouter/zhipu/glm-5.2?reasoning=medium', 'high'], - ['openrouter/zhipu/glm-5.2?reasoning=high', 'high'], - ['openrouter/zhipu/glm-5.2?reasoning=xhigh', 'max'], -] as const)('Z.AI GLM-5.2: %s enables mapped reasoning effort', async (model, effort) => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - const expectedModel = model.split('?')[0]; - let requestBody: Record | undefined +test('classifies a pre-header API timeout without replaying the request', async () => { + process.env.API_TIMEOUT_MS = '20' + const pathSecret = 'route/key+AbC123' + const encodedPathSecret = encodeURIComponent(pathSecret) + const doubleEncodedPathSecret = encodeURIComponent(encodedPathSecret) + const escapeLookingSecret = 'abc%2FdefLONG' + const encodedEscapeLookingSecret = encodeURIComponent(escapeLookingSecret) + const decodedEscapeLookingSecret = decodeURIComponent(escapeLookingSecret) + const malformedUtf8Secret = 'éSECRET_VALUE_123' + const encodedMalformedUtf8Secret = encodeURIComponent(malformedUtf8Secret) + process.env.OPENAI_API_KEY = pathSecret + process.env.OPENROUTER_API_KEY = escapeLookingSecret + process.env.DEEPSEEK_API_KEY = malformedUtf8Secret + process.env.OPENAI_BASE_URL = + `https://user:password@slow.example.test/v1/invalid%ZZ/${doubleEncodedPathSecret}/${encodedEscapeLookingSecret}/%E0%A4${encodedMalformedUtf8Secret}` + + `?prompt=${encodedPathSecret}&nested=${doubleEncodedPathSecret}` + + `&escape=${encodedEscapeLookingSecret}&malformed=%E0%A4${encodedMalformedUtf8Secret}` + + '&token=secret' + let fetchCalls = 0 + let completedGenerations = 0 + const receivedBodies: string[] = [] globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: expectedModel, - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) + fetchCalls++ + receivedBodies.push(String(init?.body)) + return new Promise((resolve, reject) => { + setTimeout(() => { + completedGenerations++ + resolve(makeChatCompletionResponse('gpt-4o-mini')) + }, 50) + const signal = init?.signal + if (!signal) return + const rejectFromAbort = () => { + reject(signal.reason ?? new DOMException('Aborted', 'AbortError')) + } + signal.addEventListener('abort', rejectFromAbort, { once: true }) + if (signal.aborted) rejectFromAbort() + }) }) as unknown as FetchType + const safety = new AbortController() + const safetyTimer = setTimeout(() => safety.abort(), 500) const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model, - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.model).toBe(expectedModel) - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe(effort) -}) - -test.each([ - 'GLM-5.1?reasoning=high', - 'GLM-4.5-Air?reasoning=high', -] as const)('Z.AI GLM: %s does not receive GLM-5.2-only reasoning_effort', async model => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model, - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, + let caught: unknown + try { + await waitForPromise( + client.beta.messages.create( + { + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, + { signal: safety.signal }, + ), + 750, + 'pre-header timeout did not settle', ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model, - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) + } catch (error) { + caught = error + } finally { + clearTimeout(safetyTimer) + } + await new Promise(resolve => setTimeout(resolve, 60)) - expect(requestBody?.model).toBe(model.split('?', 1)[0]) - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBeUndefined() + expect(caught).toBeDefined() + const error = caught as Error & { constructor: { name: string } } + expect(error.constructor.name).toBe('APIConnectionError') + expect(isOpenAIRequestNonReplayable(error)).toBe(true) + expect(error.message).toContain('no response headers within 20ms (API_TIMEOUT_MS)') + expect(error.message).toContain('slow.example.test') + expect(error.message).toContain('openai_category=request_timeout') + expect(error.message).not.toContain('password') + expect(error.message).not.toContain('token=secret') + expect(error.message).not.toContain(pathSecret) + expect(error.message).not.toContain(encodedPathSecret) + expect(error.message).not.toContain(doubleEncodedPathSecret) + expect(error.message).not.toContain(escapeLookingSecret) + expect(error.message).not.toContain(encodedEscapeLookingSecret) + expect(error.message).not.toContain(decodedEscapeLookingSecret) + expect(error.message).not.toContain(malformedUtf8Secret) + expect(error.message).not.toContain(encodedMalformedUtf8Secret) + expect(fetchCalls).toBe(1) + expect(receivedBodies).toHaveLength(1) + expect(completedGenerations).toBe(1) }) -// openaiShim test extraction seam 164 start: Z.AI GLM-5.2: model-query thinking disable omits reasoning effort -test('Z.AI GLM-5.2: model-query thinking disable omits reasoning effort', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - let requestBody: Record | undefined +test('does not proxy-retry when a deadline abort surfaces as fetch failed', async () => { + process.env.API_TIMEOUT_MS = '20' + let fetchCalls = 0 globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) + fetchCalls++ + return new Promise((_resolve, reject) => { + const signal = init?.signal + const rejectFromAbort = () => reject(new TypeError('fetch failed')) + signal?.addEventListener('abort', rejectFromAbort, { once: true }) + if (signal?.aborted) rejectFromAbort() + }) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2?thinking=disabled&reasoning=xhigh', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - }) - - expect(requestBody?.model).toBe('glm-5.2') - expect(requestBody?.thinking).toEqual({ type: 'disabled' }) - expect(requestBody?.reasoning_effort).toBeUndefined() -}) -// openaiShim test extraction seam 164 end + await expect( + client.beta.messages.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.toThrow('no response headers within 20ms (API_TIMEOUT_MS)') -// openaiShim test extraction seam 165 start: Z.AI GLM-5.2: per-turn thinking overrides model-query default -test('Z.AI GLM-5.2: per-turn thinking overrides model-query default', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' + expect(fetchCalls).toBe(1) +}) - let requestBody: Record | undefined +test('deadline wins when an abort-ignoring fetch resolves 504 afterward', async () => { + process.env.API_TIMEOUT_MS = '20' + let fetchCalls = 0 globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], - }), - { headers: { 'Content-Type': 'application/json' } }, - ) + fetchCalls++ + return new Promise(resolve => { + const resolveAfterAbort = () => { + resolve(new Response('Gateway Timeout', { status: 504 })) + } + init?.signal?.addEventListener('abort', resolveAfterAbort, { once: true }) + if (init?.signal?.aborted) resolveAfterAbort() + }) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2?thinking=disabled&reasoning=high', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: false, - thinking: { type: 'enabled' }, - }) - - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('high') -}) -// openaiShim test extraction seam 165 end - - -// openaiShim test extraction seam 166 start: NVIDIA NIM Z.AI GLM sends chat template thinking kwargs - -// openaiShim test extraction seam 166 end - - -// openaiShim test extraction seam 167 start: NVIDIA NIM Z.AI GLM omits chat template thinking kwargs without a reasoning request - -// openaiShim test extraction seam 167 end - - -// openaiShim test extraction seam 168 start: NVIDIA NIM Z.AI GLM omits chat template thinking kwargs when thinking is disabled - -// openaiShim test extraction seam 168 end + await expect( + client.beta.messages.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).rejects.toThrow('no response headers within 20ms (API_TIMEOUT_MS)') -// Extraction boundary: provider reasoning compatibility | tool-stream routing. -// The gateway emission regression below remains provider/request-shaping coverage. -// Keep this marker stable for independent adjacent test migrations. -// Regression test for #1950: GLM-5.2 served through NVIDIA NIM -// (`integrate.api.nvidia.com`) must never receive the Z.AI-proprietary -// `tool_stream` parameter. Streaming tool calls are simply not streamed on -// this gateway; sending the parameter aborts the request with -// `400 Unsupported parameter(s): tool_stream`. -// openaiShim test extraction seam 169 start: NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream (regression #1950) -test('NVIDIA NIM Z.AI GLM streaming request with tools does not send tool_stream (regression #1950)', async () => { - process.env.OPENAI_BASE_URL = 'https://integrate.api.nvidia.com/v1' - process.env.NVIDIA_API_KEY = 'nvapi-test' + expect(fetchCalls).toBe(1) +}) - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return makeSseResponse(makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'z-ai/glm-5.2', - choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'z-ai/glm-5.2', - choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], - }, - ])) +test('gives a proxy retry its own response-header deadline', async () => { + process.env.API_TIMEOUT_MS = '50' + let fetchCalls = 0 + globalThis.fetch = (async () => { + fetchCalls++ + if (fetchCalls === 1) { + await new Promise(resolve => setTimeout(resolve, 30)) + return new Response('Gateway Timeout', { status: 504 }) + } + await new Promise(resolve => setTimeout(resolve, 30)) + return makeChatCompletionResponse('gpt-4o-mini') }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'z-ai/glm-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [ - { - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, - }, - ], + const response = await client.beta.messages.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, - stream: true, + stream: false, }) - // tool_stream is a Z.AI-only streaming extension; NVIDIA NIM rejects it with - // `400 Unsupported parameter(s): tool_stream`. Streaming tool calls simply - // aren't streamed on this gateway. - expect(requestBody?.tool_stream).toBeUndefined() + expect(response).toBeDefined() + expect(fetchCalls).toBe(2) }) -// openaiShim test extraction seam 169 end +test('bounds nested URL decoding while retaining encoded secret redaction', async () => { + const secret = 'route/key+AbC123' + const secretVariants = [secret] + for (let layer = 0; layer < 4; layer++) { + secretVariants.push( + encodeURIComponent(secretVariants[secretVariants.length - 1]!), + ) + } + const deeplyNestedValue = `%${'25'.repeat(200)}` + process.env.OPENAI_API_KEY = secret + process.env.OPENAI_BASE_URL = + `https://slow.example.test/v1/${deeplyNestedValue}/${secretVariants[4]}` + globalThis.fetch = asMockFetch(mock(async () => { + throw new TypeError('fetch failed') + })) -// Extraction boundary: provider tool-stream shaping | executor tool-stream retry. -// The three retry-state tests below move together with request execution. -// Keep this marker stable for independent adjacent test migrations. -// Regression test for #1950: even if a gateway rejects `tool_stream` with a -// 400 (e.g. NVIDIA NIM: `Unsupported parameter(s): tool_stream`), the shim -// self-heals by dropping only that parameter and retrying with tools intact. -// Here we exercise the generic self-heal using a Z.AI-contract gateway that -// actually sends `tool_stream`, then rejects it — proving the retry drops the -// parameter rather than surfacing a hard error. -// openaiShim test extraction seam 170 start: Shim self-heals a JSON `tool_stream` rejection by retrying without it (#1950) -test('Shim self-heals a JSON `tool_stream` rejection by retrying without it (#1950)', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - const requestBodies: Array> = [] - let callCount = 0 - globalThis.fetch = (async (_input, init) => { - requestBodies.push(JSON.parse(String(init?.body))) - callCount += 1 - if (callCount === 1) { - return new Response( - '{"error":{"message":"tool_stream is unsupported"}}', - { status: 400, headers: { 'Content-Type': 'application/json' } }, - ) - } - return makeSseResponse(makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], - }, - ])) - }) as unknown as FetchType + const originalDecodeURIComponent = globalThis.decodeURIComponent + let decodeCalls = 0 + globalThis.decodeURIComponent = ((value: string) => { + decodeCalls++ + return originalDecodeURIComponent(value) + }) as typeof decodeURIComponent const client = createOpenAIShimClient({}) as OpenAIShimClient - // Must not throw — the self-heal retry succeeds. - await client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [ - { - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, - }, - ], - max_tokens: 64, - stream: true, - }) + let caught: unknown + try { + await client.beta.messages.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + } catch (error) { + caught = error + } finally { + globalThis.decodeURIComponent = originalDecodeURIComponent + } - // First attempt sent tool_stream; the self-heal dropped it and retried. - expect(requestBodies).toHaveLength(2) - expect(requestBodies[0]?.tool_stream).toBe(true) - expect(requestBodies[1]?.tool_stream).toBeUndefined() - // Tools are preserved across the retry. - expect(Array.isArray(requestBodies[1]?.tools)).toBe(true) + expect(caught).toBeDefined() + for (const secretVariant of secretVariants) { + expect((caught as Error).message).not.toContain(secretVariant) + } + expect(decodeCalls).toBeLessThanOrEqual(32) }) -// openaiShim test extraction seam 170 end +test('decodes malformed URL escape runs in linear work', async () => { + const secret = 'route/key+AbC123' + const encodedSecret = encodeURIComponent(secret) + const malformedEscapeCount = 200 + const malformedUtf8Run = '%80'.repeat(malformedEscapeCount) + process.env.OPENAI_API_KEY = secret + process.env.OPENAI_BASE_URL = + `https://slow.example.test/v1/${malformedUtf8Run}/${encodedSecret}` + globalThis.fetch = asMockFetch(mock(async () => { + throw new TypeError('fetch failed') + })) -// openaiShim test extraction seam 171 start: Shim stops after one tool_stream self-heal retry when the retry also fails (#1950) -test('Shim stops after one tool_stream self-heal retry when the retry also fails (#1950)', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - - const requestBodies: Array> = [] - globalThis.fetch = (async (_input, init) => { - requestBodies.push(JSON.parse(String(init?.body))) - return new Response( - '{"error":{"message":"tool_stream is unsupported"}}', - { status: 400, headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType + const originalDecodeURIComponent = globalThis.decodeURIComponent + let malformedDecodeCalls = 0 + globalThis.decodeURIComponent = ((value: string) => { + if (value.includes('%80')) malformedDecodeCalls++ + return originalDecodeURIComponent(value) + }) as typeof decodeURIComponent const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [{ - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, - }], + let caught: unknown + try { + await client.beta.messages.create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, - stream: true, - }), - ).rejects.toThrow() + stream: false, + }) + } catch (error) { + caught = error + } finally { + globalThis.decodeURIComponent = originalDecodeURIComponent + } - expect(requestBodies).toHaveLength(2) - expect(requestBodies[0]?.tool_stream).toBe(true) - expect(requestBodies[1]?.tool_stream).toBeUndefined() + expect(caught).toBeDefined() + expect((caught as Error).message).not.toContain(secret) + expect((caught as Error).message).not.toContain(encodedSecret) + expect(malformedDecodeCalls).toBeLessThanOrEqual(malformedEscapeCount * 10) }) -// openaiShim test extraction seam 171 end - - -// openaiShim test extraction seam 172 start: Shim retries a tool_stream rejection with the same pooled credential (#1950) -test('Shim retries a tool_stream rejection with the same pooled credential (#1950)', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEYS = 'key-a,key-b' - delete process.env.OPENAI_API_KEY - const authorizations: Array = [] - let callCount = 0 +test('preserves caller cancellation while waiting for response headers without retrying', async () => { + process.env.API_TIMEOUT_MS = '200' + let fetchCalls = 0 globalThis.fetch = (async (_input, init) => { - const headers = init?.headers as Record | undefined - authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) - callCount += 1 - if (callCount === 1) { - return new Response( - '{"error":{"message":"Validation: Unsupported parameter(s): `tool_stream`"}}', - { status: 400, headers: { 'Content-Type': 'application/json' } }, - ) - } - return makeSseResponse(makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], - }, - ])) + fetchCalls++ + return pendingFetchUntilAbort(init) }) as unknown as FetchType + const caller = new AbortController() + const callerReason = new DOMException('Cancelled by user', 'AbortError') const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [{ - name: 'Bash', - description: 'Run a shell command', - input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] }, - }], - max_tokens: 64, - stream: true, + const originalAbortSignalAny = Object.getOwnPropertyDescriptor( + AbortSignal, + 'any', + ) + Object.defineProperty(AbortSignal, 'any', { + value: undefined, + configurable: true, }) + try { + const request = client.beta.messages.create( + { + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, + { signal: caller.signal }, + ) - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-a']) -}) -// openaiShim test extraction seam 172 end - + setTimeout(() => caller.abort(callerReason), 10) -// Extraction boundary: executor tool-stream retry | provider tool-stream shaping. -// Provider emission rules below remain with compatibility/request planning. -// Keep this marker stable for independent adjacent test migrations. -// openaiShim test extraction seam 173 start: Z.AI GLM-5.2: streaming requests with tools send tool_stream -test('Z.AI GLM-5.2: streaming requests with tools send tool_stream', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' + await expect( + waitForPromise(request, 500, 'caller abort did not settle'), + ).rejects.toBe(callerReason) + expect(fetchCalls).toBe(1) + } finally { + if (originalAbortSignalAny) { + Object.defineProperty(AbortSignal, 'any', originalAbortSignalAny) + } + } +}) - let requestBody: Record | undefined +test('native signal composition preserves the caller abort reason when fetch rejects AbortError', async () => { + expect(typeof AbortSignal.any).toBe('function') + process.env.API_TIMEOUT_MS = '200' + let fetchCalls = 0 + const fetchAbortError = new DOMException( + 'The operation was aborted.', + 'AbortError', + ) globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return makeSseResponse(makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], - }, - ])) + fetchCalls++ + return new Promise((_resolve, reject) => { + const signal = init?.signal + if (!signal) return + const rejectFromAbort = () => reject(fetchAbortError) + signal.addEventListener('abort', rejectFromAbort, { once: true }) + if (signal.aborted) rejectFromAbort() + }) }) as unknown as FetchType + const caller = new AbortController() + const callerReason = new DOMException('Cancelled by user', 'AbortError') const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [ - { - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, - }, - ], - max_tokens: 64, - stream: true, - }) - - expect(requestBody?.tool_stream).toBe(true) -}) -// openaiShim test extraction seam 173 end + const request = client.beta.messages.create( + { + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }, + { signal: caller.signal }, + ) + const abortTimer = setTimeout(() => caller.abort(callerReason), 10) -// openaiShim test extraction seam 174 start: Hicap GLM-5.2: uses Z.AI-compatible request shaping -test('Hicap GLM-5.2: uses Z.AI-compatible request shaping', async () => { - process.env.OPENAI_BASE_URL = 'https://api.hicap.ai/v1' - process.env.HICAP_API_KEY = 'sk-hicap-test' + try { + await expect( + waitForPromise(request, 500, 'caller abort did not settle'), + ).rejects.toBe(callerReason) + } finally { + clearTimeout(abortTimer) + } + expect(fetchCalls).toBe(1) +}) - let requestBody: Record | undefined +test('caller abort winning the timeout catch race prevents a retry', async () => { + process.env.API_TIMEOUT_MS = '20' + let fetchCalls = 0 + const caller = new AbortController() + const callerReason = new DOMException('Cancelled by user', 'AbortError') globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - return makeSseResponse(makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], - }, - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model: 'glm-5.2', - choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], - }, - ])) + fetchCalls++ + return new Promise((_resolve, reject) => { + const signal = init?.signal + if (!signal) return + const rejectFromAbort = () => { + reject(signal.reason ?? new DOMException('Aborted', 'AbortError')) + if (fetchCalls === 1) { + queueMicrotask(() => caller.abort(callerReason)) + } + } + signal.addEventListener('abort', rejectFromAbort, { once: true }) + if (signal.aborted) rejectFromAbort() + }) }) as unknown as FetchType - const client = createOpenAIShimClient({ reasoningEffort: 'xhigh' }) as OpenAIShimClient - await client.beta.messages.create({ - model: 'GLM-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [ - { - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + waitForPromise( + client.beta.messages.create( + { + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, }, - }, - ], - max_tokens: 64, - stream: true, - }) - - expect(requestBody?.model).toBe('glm-5.2') - expect(requestBody?.store).toBeUndefined() - expect(requestBody?.max_tokens).toBe(64) - expect(requestBody?.max_completion_tokens).toBeUndefined() - expect(requestBody?.thinking).toEqual({ type: 'enabled' }) - expect(requestBody?.reasoning_effort).toBe('max') - expect(requestBody?.tool_stream).toBe(true) -}) -// openaiShim test extraction seam 174 end - -// openaiShim test extraction seam 175 start: Z.AI GLM-5.2: remote tool incompatibility does not use local toolless retry -test('Z.AI GLM-5.2: remote tool incompatibility does not use local toolless retry', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' + { signal: caller.signal }, + ), + 500, + 'caller abort race did not settle', + ), + ).rejects.toBe(callerReason) + expect(fetchCalls).toBe(1) +}) - const requestBodies: Array> = [] +test('manual signal fallback preserves caller cancellation after headers arrive', async () => { + process.env.API_TIMEOUT_MS = '200' + const fetchSignals: AbortSignal[] = [] + const stalled = makeStallingResponse( + makeOpenAIStreamFrame({ role: 'assistant', content: 'started' }), + ) globalThis.fetch = (async (_input, init) => { - requestBodies.push(JSON.parse(String(init?.body)) as Record) - return new Response('tool_calls are not supported', { - status: 400, - headers: { 'Content-Type': 'text/plain' }, - }) + if (init?.signal) fetchSignals.push(init.signal) + return stalled.response }) as unknown as FetchType + const caller = new AbortController() const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'glm-5.2', - messages: [{ role: 'user', content: 'run pwd' }], - tools: [ + const originalAbortSignalAny = Object.getOwnPropertyDescriptor( + AbortSignal, + 'any', + ) + Object.defineProperty(AbortSignal, 'any', { + value: undefined, + configurable: true, + }) + try { + const result = await client.beta.messages + .create( { - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, }, - ], - max_tokens: 64, - stream: true, - }), - ).rejects.toThrow() + { signal: caller.signal }, + ) + .withResponse() - expect(requestBodies).toHaveLength(1) - expect(requestBodies[0]?.tool_stream).toBe(true) -}) -// openaiShim test extraction seam 175 end + await expectAbortStopsStream({ + abort: () => caller.abort(), + cancelReasons: stalled.cancelReasons, + expectedEventsBeforeAbort: 1, + label: 'manual combined signal after headers', + stream: result.data as ShimStream, + }) + expect(fetchSignals).toHaveLength(1) + expect(fetchSignals[0].aborted).toBe(true) + } finally { + stalled.close() + if (originalAbortSignalAny) { + Object.defineProperty(AbortSignal, 'any', originalAbortSignalAny) + } + } +}) -test.each([ - ['non-streaming Z.AI request with tools', 'https://api.z.ai/api/coding/paas/v4', false, true, 'glm-5.2'], - ['streaming Z.AI request without tools', 'https://api.z.ai/api/coding/paas/v4', true, false, 'glm-5.2'], - ['streaming non-Z.AI request with tools', 'https://api.openai.com/v1', true, true, 'gpt-4o'], -] as const)('does not send tool_stream for %s', async (_name, baseUrl, stream, includeTools, model) => { - process.env.OPENAI_BASE_URL = baseUrl - process.env.OPENAI_API_KEY = 'sk-test' +test('manual signal fallback removes caller forwarding after the body settles', async () => { + process.env.API_TIMEOUT_MS = '200' + globalThis.fetch = asMockFetch(mock(async () => + makeChatCompletionResponse('gpt-4o-mini'))) - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) - if (stream) { - return makeSseResponse(makeStreamChunks([ - { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model, - choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], - }, + const caller = new AbortController() + const client = createOpenAIShimClient({}) as OpenAIShimClient + const originalAbortSignalAny = Object.getOwnPropertyDescriptor( + AbortSignal, + 'any', + ) + Object.defineProperty(AbortSignal, 'any', { + value: undefined, + configurable: true, + }) + try { + for (let request = 0; request < 2; request++) { + await client.beta.messages.create( { - id: 'chatcmpl-1', - object: 'chat.completion.chunk', - model, - choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, }, - ])) + { signal: caller.signal }, + ) + expect(getEventListeners(caller.signal, 'abort')).toHaveLength(0) + } + } finally { + if (originalAbortSignalAny) { + Object.defineProperty(AbortSignal, 'any', originalAbortSignalAny) } + } +}) + +test('disarms the API timeout after headers arrive while the body keeps streaming', async () => { + process.env.API_TIMEOUT_MS = '20' + const fetchSignals: AbortSignal[] = [] + const encoder = new TextEncoder() + globalThis.fetch = (async (_input, init) => { + if (init?.signal) fetchSignals.push(init.signal) return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model, - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], + new ReadableStream({ + start(controller) { + setTimeout(() => { + controller.enqueue(encoder.encode(makeOpenAIStreamFrame( + { role: 'assistant', content: 'late body' }, + 'stop', + ))) + controller.enqueue(encoder.encode('data: [DONE]\n\n')) + controller.close() + }, 50) + }, }), - { headers: { 'Content-Type': 'application/json' } }, + { headers: { 'Content-Type': 'text/event-stream' } }, ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model, - messages: [{ role: 'user', content: 'hi' }], - tools: includeTools - ? [ - { - name: 'Bash', - description: 'Run a shell command', - input_schema: { - type: 'object', - properties: { command: { type: 'string' } }, - required: ['command'], - }, - }, - ] - : undefined, - max_tokens: 64, - stream, - }) + const result = await client.beta.messages + .create({ + model: 'gpt-4o-mini', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: true, + }) + .withResponse() - expect(requestBody?.tool_stream).toBeUndefined() + const events: Array> = [] + for await (const event of result.data) events.push(event) + + expect(events.length).toBeGreaterThan(0) + expect(fetchSignals).toHaveLength(1) + expect(fetchSignals[0].aborted).toBe(false) }) +// openaiShim test extraction seam 128 end -// openaiShim test extraction seam 176 start: Z.AI GLM-5.2: preserved thinking round-trips with tool calls -test('Z.AI GLM-5.2: preserved thinking round-trips with tool calls', async () => { - process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' - process.env.OPENAI_API_KEY = 'sk-zai-test' - let requestBody: Record | undefined - globalThis.fetch = (async (_input, init) => { - requestBody = JSON.parse(String(init?.body)) +// Extraction boundary: executor network behavior | native Ollama routing. +// Native Ollama endpoint selection remains an adapter/facade integration concern. +// Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 129 start: uses native Ollama chat endpoint when local base URL omits /v1 +test('uses native Ollama chat endpoint when local base URL omits /v1', async () => { + process.env.OPENAI_BASE_URL = 'http://localhost:11434' + + const requestUrls: string[] = [] + globalThis.fetch = (async (input, _init) => { + const url = typeof input === 'string' ? input : input.url + requestUrls.push(url) + return new Response( JSON.stringify({ - id: 'chatcmpl-1', - model: 'glm-5.2', - choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, - ], + model: 'qwen2.5-coder:7b', + message: { + role: 'assistant', + content: 'hello from native Ollama', + }, + done: true, + done_reason: 'stop', + prompt_eval_count: 5, + eval_count: 2, }), - { headers: { 'Content-Type': 'application/json' } }, + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await client.beta.messages.create({ - model: 'glm-5.2', - messages: [ - { role: 'user', content: 'inspect files' }, - { - role: 'assistant', - content: [ - { type: 'thinking', thinking: 'Need to list files before answering.' }, - { - type: 'tool_use', - id: 'call_bash_1', - name: 'Bash', - input: { command: 'ls' }, - }, - ], - }, - { - role: 'user', - content: [ - { type: 'tool_result', tool_use_id: 'call_bash_1', content: 'README.md' }, - ], - }, - ], - max_tokens: 64, - stream: false, - }) - const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - message => message.role === 'assistant' && Array.isArray(message.tool_calls), - ) + await expect( + client.beta.messages.create({ + model: 'qwen2.5-coder:7b', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }), + ).resolves.toBeDefined() - expect(assistantWithToolCall?.reasoning_content).toBe( - 'Need to list files before answering.', - ) - expect(assistantWithToolCall?.tool_calls).toEqual([ - { - id: 'call_bash_1', - type: 'function', - function: { - name: 'Bash', - arguments: JSON.stringify({ command: 'ls' }), - }, - }, - ]) + expect(requestUrls).toEqual(['http://localhost:11434/api/chat']) }) -// openaiShim test extraction seam 176 end +// openaiShim test extraction seam 132 end -// openaiShim test extraction seam 177 start: strips Anthropic attribution header block from chat-completions system prompt (#607) -test('strips Anthropic attribution header block from chat-completions system prompt (#607)', async () => { - let capturedBody: Record | undefined +// Extraction boundary: executor tool self-healing | message conversion. +// Message-history normalization below belongs to the message converter. +// Keep this marker stable for independent adjacent test migrations. +// openaiShim test extraction seam 133 start: preserves valid tool_result and drops orphan tool_result +test('preserves valid tool_result and drops orphan tool_result', async () => { + let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { - capturedBody = JSON.parse(String(init?.body)) as Record + requestBody = JSON.parse(String(init?.body)) return new Response( JSON.stringify({ id: 'chatcmpl-1', - model: 'gpt-4o', + model: 'mistral-large-latest', choices: [ { - message: { role: 'assistant', content: 'ok' }, + message: { + role: 'assistant', + content: 'done', + }, finish_reason: 'stop', }, ], - usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }, + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, }), - { headers: { 'Content-Type': 'application/json' } }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'gpt-4o', - system: [ + model: 'mistral-large-latest', + system: 'test system', + messages: [ + { role: 'user', content: 'Search and then I will interrupt' }, { - type: 'text', - text: - 'x-anthropic-billing-header: cc_version=0.8.0.abc123; ' + - 'cc_entrypoint=cli;', - }, - { type: 'text', text: 'You are Claude Code, helpful assistant.' }, - { type: 'text', text: 'Project context: bun + react.' }, - ], - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 64, - stream: false, - }) - - const messages = capturedBody?.messages as Array<{ role: string; content: string }> - const sysMsg = messages.find(m => m.role === 'system') - expect(sysMsg).toBeDefined() - expect(sysMsg?.content).not.toContain('x-anthropic-billing-header') - expect(sysMsg?.content).not.toContain('cc_version=') - expect(sysMsg?.content).toContain('You are Claude Code, helpful assistant.') - expect(sysMsg?.content).toContain('Project context: bun + react.') -}) -// openaiShim test extraction seam 177 end - - -// openaiShim test extraction seam 178 start: strips Anthropic attribution header block from responses-API instructions (#607) -test('strips Anthropic attribution header block from responses-API instructions (#607)', async () => { - process.env.OPENAI_API_FORMAT = 'responses' - let capturedBody: Record | undefined - - globalThis.fetch = (async (_input, init) => { - capturedBody = JSON.parse(String(init?.body)) as Record - - return new Response( - JSON.stringify({ - id: 'resp-1', - model: 'gpt-5.4', - output: [ + role: 'assistant', + content: [ { - type: 'message', - role: 'assistant', - content: [{ type: 'output_text', text: 'ok' }], + type: 'tool_use', + id: 'valid_call_1', + name: 'Search', + input: { query: 'openclaude' }, }, ], - usage: { input_tokens: 8, output_tokens: 3, total_tokens: 11 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType - - const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient - - await client.beta.messages.create({ - model: 'gpt-5.4', - system: [ + }, { - type: 'text', - text: 'x-anthropic-billing-header: cc_version=0.8.0.abc123; cc_entrypoint=cli;', + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'valid_call_1', + content: 'Found it!', + }, + { + type: 'tool_result', + tool_use_id: 'orphan_call_2', + content: 'Interrupted result', + }, + { + role: 'user', + content: 'What happened?', + }, + ], }, - { type: 'text', text: 'You are Claude Code.' }, ], - messages: [{ role: 'user', content: 'hello' }], max_tokens: 64, stream: false, }) - const instructions = capturedBody?.instructions as string - expect(instructions).not.toContain('x-anthropic-billing-header') - expect(instructions).not.toContain('cc_version=') - expect(instructions).toContain('You are Claude Code.') -}) -// openaiShim test extraction seam 178 end + const messages = requestBody?.messages as Array> + // Should have: system, user, assistant (tool_use), tool (valid_call_1), user + // Should NOT have: tool (orphan_call_2) -// openaiShim test extraction seam 179 start: emits reasoning_effort on chat_completions when reasoningEffort is passed -test('emits reasoning_effort on chat_completions when reasoningEffort is passed', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_API_KEY = 'test-key' - // gpt-5.4 now auto-routes to /responses on api.openai.com; opt back into - // chat_completions to exercise its top-level reasoning_effort serialization. - process.env.OPENAI_API_FORMAT = 'chat_completions' + const toolMessages = messages.filter(m => m.role === 'tool') + expect(toolMessages.length).toBe(1) + expect(toolMessages[0].tool_call_id).toBe('valid_call_1') + + const orphanMessage = toolMessages.find(m => m.tool_call_id === 'orphan_call_2') + expect(orphanMessage).toBeUndefined() + // Actually, the semantic message IS injected here because the user block with orphan + // tool result is converted to: + // 1. Tool result (valid_call_1) -> role 'tool' + // 2. User content ("What happened?") -> role 'user' + // This triggers the tool -> assistant injection. + const assistantMessages = messages.filter(m => m.role === 'assistant') + expect(assistantMessages.some(m => m.content === '[Tool results received]')).toBe(true) +}) +// openaiShim test extraction seam 133 end + +// openaiShim test extraction seam 134 start: drops empty assistant message when only thinking block was present and stripped +test('drops empty assistant message when only thinking block was present and stripped', async () => { let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-5.4', - choices: [ - { - message: { role: 'assistant', content: 'ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) + return new Response(JSON.stringify({ + id: 'chatcmpl-1', + object: 'chat.completion', + created: 123456789, + model: 'mistral-large-latest', + choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } + }), { headers: { 'Content-Type': 'application/json' } }) }) as unknown as FetchType - const client = createOpenAIShimClient({ - reasoningEffort: 'xhigh', - }) as OpenAIShimClient + const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'gpt-5.4', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 16, + model: 'mistral-large-latest', + messages: [ + { role: 'user', content: 'Initial' }, + { role: 'assistant', content: [{ type: 'thinking', thinking: 'I am thinking...', signature: 'sig' }] }, + { role: 'user', content: 'Interrupting query' }, + ], + max_tokens: 64, stream: false, }) - expect(requestBody?.reasoning_effort).toBe('xhigh') + const messages = requestBody?.messages as Array> + // The assistant msg is dropped because thinking is stripped. + // The two user messages are coalesced. + expect(messages.length).toBe(1) + expect(messages[0].role).toBe('user') + expect(String(messages[0].content)).toContain('Initial') + expect(String(messages[0].content)).toContain('Interrupting query') }) -// openaiShim test extraction seam 179 end - +// openaiShim test extraction seam 134 end -// openaiShim test extraction seam 180 start: omits reasoning_effort on chat_completions when no override and model has no alias default -test('omits reasoning_effort on chat_completions when no override and model has no alias default', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_API_KEY = 'test-key' +// openaiShim test extraction seam 135 start: drops empty assistant message when only redacted_thinking block was present and stripped +test('drops empty assistant message when only redacted_thinking block was present and stripped', async () => { let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-4o', - choices: [ - { - message: { role: 'assistant', content: 'ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) - }) as unknown as FetchType + return new Response(JSON.stringify({ + id: 'chatcmpl-1', + object: 'chat.completion', + created: 123456789, + model: 'mistral-large-latest', + choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } + }), { headers: { 'Content-Type': 'application/json' } }) + }) as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'gpt-4o', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 16, + model: 'mistral-large-latest', + messages: [ + { role: 'user', content: 'Initial' }, + { role: 'assistant', content: [{ type: 'redacted_thinking', data: '[thinking hidden]' }] }, + { role: 'user', content: 'Interrupting query' }, + ], + max_tokens: 64, stream: false, }) - expect(requestBody && 'reasoning_effort' in requestBody).toBe(false) + const messages = requestBody?.messages as Array> + // The assistant msg is dropped because redacted_thinking is stripped. + // The two user messages are coalesced. + expect(messages.length).toBe(1) + expect(messages[0].role).toBe('user') + expect(String(messages[0].content)).toContain('Initial') + expect(String(messages[0].content)).toContain('Interrupting query') }) -// openaiShim test extraction seam 180 end - +// openaiShim test extraction seam 135 end -// openaiShim test extraction seam 181 start: emits reasoning_effort from codex alias default when no override is passed -test('emits reasoning_effort from codex alias default when no override is passed', async () => { - process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' - process.env.OPENAI_API_KEY = 'test-key' - // gpt-5.4 now auto-routes to /responses on api.openai.com; opt back into - // chat_completions to exercise its top-level reasoning_effort serialization. - process.env.OPENAI_API_FORMAT = 'chat_completions' +// openaiShim test extraction seam 136 start: injects semantic assistant message when tool result is followed by user message +test('injects semantic assistant message when tool result is followed by user message', async () => { let requestBody: Record | undefined globalThis.fetch = (async (_input, init) => { requestBody = JSON.parse(String(init?.body)) - return new Response( - JSON.stringify({ - id: 'chatcmpl-1', - model: 'gpt-5.4', - choices: [ - { - message: { role: 'assistant', content: 'ok' }, - finish_reason: 'stop', - }, - ], - usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, - }), - { headers: { 'Content-Type': 'application/json' } }, - ) + return new Response(JSON.stringify({ + id: 'chatcmpl-2', + object: 'chat.completion', + created: 123456789, + model: 'mistral-large-latest', + choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } + }), { headers: { 'Content-Type': 'application/json' } }) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'gpt-5.4', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 16, + model: 'mistral-large-latest', + messages: [ + { + role: 'assistant', + content: [{ type: 'tool_use', id: 'call_1', name: 'search', input: {} }] + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_1', content: 'Result' } + ] + }, + { role: 'user', content: 'Next user query' }, + ], + max_tokens: 64, stream: false, }) - expect(requestBody?.reasoning_effort).toBe('high') + const messages = requestBody?.messages as Array> + // Roles should be: assistant (tool_calls) -> tool -> assistant (semantic) -> user + const roles = messages.map(m => m.role) + expect(roles).toEqual(['assistant', 'tool', 'assistant', 'user']) + + const semanticMsg = messages[2] + expect(semanticMsg.role).toBe('assistant') + expect(semanticMsg.content).toBe('[Tool results received]') + expect(semanticMsg.content).not.toContain('interrupted') + expect(semanticMsg.content).not.toContain('user') }) -// openaiShim test extraction seam 181 end +// openaiShim test extraction seam 156 end -// openaiShim test extraction seam 182 start: DeepSeek: redacted_thinking block preserves continuity with reasoning_content: "" -test('DeepSeek: redacted_thinking block preserves continuity with reasoning_content: ""', async () => { - process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' - process.env.OPENAI_API_KEY = 'sk-deepseek' +// openaiShim test extraction seam 157 start: collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774) +test('collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774)', async () => { let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { requestBody = JSON.parse(String(init?.body)) + return new Response( JSON.stringify({ id: 'chatcmpl-1', - model: 'deepseek-chat', + model: 'deepseek-reasoner', choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + { + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, }), - { headers: { 'Content-Type': 'application/json' } }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ - model: 'deepseek-chat', - system: 'test', + model: 'deepseek-reasoner', + system: 'test system', messages: [ - { role: 'user', content: 'analyze this' }, + { role: 'user', content: 'Run ls' }, { role: 'assistant', content: [ - // real redacted_thinking shape: content lives in `.data`, not `.thinking` - { type: 'redacted_thinking', data: '', signature: 'sig123' }, - { type: 'text', text: 'Analysis complete.' }, { type: 'tool_use', - id: 'call_redacted_1', + id: 'call_1', name: 'Bash', input: { command: 'ls' }, }, @@ -10626,164 +5239,141 @@ test('DeepSeek: redacted_thinking block preserves continuity with reasoning_cont { role: 'user', content: [ - { type: 'tool_result', tool_use_id: 'call_redacted_1', content: 'files' }, + { + type: 'tool_result', + tool_use_id: 'call_1', + content: [ + { type: 'text', text: 'line one' }, + { type: 'text', text: 'line two' }, + ], + }, ], }, ], - max_tokens: 32, + max_tokens: 64, stream: false, }) const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - m => m.role === 'assistant' && Array.isArray(m.tool_calls), - ) - expect(assistantWithToolCall).toBeDefined() - // redacted_thinking is recognized as a thinking block; its .data is "" and the - // message carries a tool_call, so it falls back to reasoning_content: "" - expect(assistantWithToolCall?.reasoning_content).toBe('') + const toolMessages = messages.filter(m => m.role === 'tool') + expect(toolMessages.length).toBe(1) + expect(toolMessages[0].tool_call_id).toBe('call_1') + expect(typeof toolMessages[0].content).toBe('string') + expect(toolMessages[0].content).toBe('line one\n\nline two') }) -// openaiShim test extraction seam 182 end - +// openaiShim test extraction seam 157 end -// openaiShim test extraction seam 183 start: DeepSeek: redacted_thinking block with non-empty data propagates data into reasoning_content -test('DeepSeek: redacted_thinking block with non-empty data propagates data into reasoning_content', async () => { - process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' - process.env.OPENAI_API_KEY = 'sk-deepseek' +// openaiShim test extraction seam 158 start: collapses multiple text blocks into a single string for DeepSeek compatibility (issue #774) +test('collapses multiple text blocks into a single string for DeepSeek compatibility (issue #774)', async () => { let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { requestBody = JSON.parse(String(init?.body)) + return new Response( JSON.stringify({ - id: 'chatcmpl-2', - model: 'deepseek-chat', + id: 'chatcmpl-1', + model: 'deepseek-reasoner', choices: [ - { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + { + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', + }, ], - usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, }), - { headers: { 'Content-Type': 'application/json' } }, + { + headers: { + 'Content-Type': 'application/json', + }, + }, ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ - model: 'deepseek-chat', - system: 'test', + model: 'deepseek-reasoner', + system: 'test system', messages: [ - { role: 'user', content: 'analyze this' }, - { - role: 'assistant', - content: [ - // real redacted_thinking with content in .data - { - type: 'redacted_thinking', - data: 'encrypted_chain_of_thought_payload_v1', - signature: 'sig456', - }, - { type: 'text', text: 'Analysis complete.' }, - { - type: 'tool_use', - id: 'call_redacted_2', - name: 'Bash', - input: { command: 'ls' }, - }, - ], - }, { role: 'user', content: [ - { type: 'tool_result', tool_use_id: 'call_redacted_2', content: 'files' }, + { type: 'text', text: 'Hello!' }, + { type: 'text', text: 'How are you?' }, ], }, ], - max_tokens: 32, + max_tokens: 64, stream: false, }) const messages = requestBody?.messages as Array> - const assistantWithToolCall = messages.find( - m => m.role === 'assistant' && Array.isArray(m.tool_calls), - ) - expect(assistantWithToolCall).toBeDefined() - // The real .data payload must be preserved in reasoning_content — this is the - // case the original test missed (it used a synthetic .thinking field). - expect(assistantWithToolCall?.reasoning_content).toBe( - 'encrypted_chain_of_thought_payload_v1', - ) + expect(messages.length).toBe(2) // system + user + expect(messages[1].role).toBe('user') + expect(typeof messages[1].content).toBe('string') + expect(messages[1].content).toBe('Hello!\n\nHow are you?') }) -// openaiShim test extraction seam 183 end +// openaiShim test extraction seam 158 end -// openaiShim test extraction seam 184 start: renders tool_reference blocks as text on the chat/completions path -test('renders tool_reference blocks as text on the chat/completions path', async () => { - const { __test } = await import('./openaiShim.ts') +// openaiShim test extraction seam 159 start: preserves mixed text and image tool results as multipart content +test('preserves mixed text and image tool results as multipart content', async () => { + let requestBody: Record | undefined - const messages = __test.convertMessages( - [ - { - role: 'assistant', - content: [ - { type: 'tool_use', id: 'call_ts1', name: 'ToolSearch', input: { query: 'memory' } }, - ], - }, - { - role: 'user', - content: [ + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-4o', + choices: [ { - type: 'tool_result', - tool_use_id: 'call_ts1', - content: [ - { type: 'tool_reference', tool_name: 'mcp__example__memory_search' }, - { type: 'tool_reference', tool_name: 'mcp__example__memory_store' }, - ], + message: { + role: 'assistant', + content: 'done', + }, + finish_reason: 'stop', }, ], + usage: { + prompt_tokens: 12, + completion_tokens: 4, + total_tokens: 16, + }, + }), + { + headers: { + 'Content-Type': 'application/json', + }, }, - ], - undefined, - ) - - const toolMsg = messages.find(m => m.role === 'tool') - expect(toolMsg).toBeDefined() - // The rendering contract is plain text: text-only parts collapse to a string. - expect(typeof toolMsg!.content).toBe('string') - const content = toolMsg!.content as string - expect(content).toContain('mcp__example__memory_search') - expect(content).toContain('mcp__example__memory_store') -}) -// openaiShim test extraction seam 184 end - + ) + }) as unknown as FetchType -// openaiShim test extraction seam 185 start: preserves valid tool pairs after history pruning while dropping orphaned tool calls -test('preserves valid tool pairs after history pruning while dropping orphaned tool calls', async () => { - const { __test } = await import('./openaiShim.ts') + const client = createOpenAIShimClient({}) as OpenAIShimClient - const messages = __test.convertMessages( - [ - { role: 'user', content: 'compacted summary of previous work' }, - { - role: 'assistant', - content: [ - { - type: 'tool_use', - id: 'call_pruned_without_result', - name: 'Read', - input: { file_path: 'old.ts' }, - }, - ], - }, - { role: 'user', content: 'continue with retained context' }, + await client.beta.messages.create({ + model: 'gpt-4o', + system: 'test system', + messages: [ + { role: 'user', content: 'Show me' }, { role: 'assistant', content: [ - { type: 'text', text: 'Reading the current file.' }, { type: 'tool_use', - id: 'call_retained', - name: 'Read', - input: { file_path: 'current.ts' }, + id: 'call_1', + name: 'Bash', + input: { command: 'cat image.png' }, }, ], }, @@ -10792,665 +5382,714 @@ test('preserves valid tool pairs after history pruning while dropping orphaned t content: [ { type: 'tool_result', - tool_use_id: 'call_retained', - content: 'current contents', + tool_use_id: 'call_1', + content: [ + { type: 'text', text: 'Here is the image:' }, + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: 'iVBORw0KGgo=', + }, + }, + ], }, ], }, ], - undefined, - ) - - const toolCalls = messages.flatMap(message => message.tool_calls ?? []) - expect(toolCalls.map(toolCall => toolCall.id)).toEqual(['call_retained']) + max_tokens: 64, + stream: false, + }) - const toolMessages = messages.filter(message => message.role === 'tool') - expect(toolMessages).toHaveLength(1) - expect(toolMessages[0]?.tool_call_id).toBe('call_retained') + const messages = requestBody?.messages as Array> + const toolMessages = messages.filter(m => m.role === 'tool') + expect(toolMessages.length).toBe(1) + expect(Array.isArray(toolMessages[0].content)).toBe(true) + const content = toolMessages[0].content as Array> + expect(content.length).toBe(2) + expect(content[0].type).toBe('text') + expect(content[1].type).toBe('image_url') }) -// openaiShim test extraction seam 185 end - - -// Extraction boundary: history pruning | executor Copilot refresh behavior. -// The contiguous Copilot authentication retry block below moves with execution. -// Keep this marker stable for independent adjacent test migrations. -function makeCodexSseResponse(responseData: Record): Response { - const data = JSON.stringify(responseData) - return makeSseResponse([`event: response.completed\ndata: ${data}\n\n`]) -} +// openaiShim test extraction seam 163 end -test('GitHub Copilot codex responses transport does not replay after a pre-header timeout', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'test-token' - process.env.API_TIMEOUT_MS = '20' - let fetchCalls = 0 - const requestUrls: string[] = [] - globalThis.fetch = (async (input, init) => { - fetchCalls++ - requestUrls.push(String(input)) - return pendingFetchUntilAbort(init) - }) as unknown as FetchType +test.each([ + ['glm-5.2?reasoning=low', 'high'], + ['glm-5.2?reasoning=medium', 'high'], + ['glm-5.2?reasoning=high', 'high'], + ['glm-5.2?reasoning=xhigh', 'max'], + ['openrouter/zhipu/glm-5.2?reasoning=low', 'high'], + ['openrouter/zhipu/glm-5.2?reasoning=medium', 'high'], + ['openrouter/zhipu/glm-5.2?reasoning=high', 'high'], + ['openrouter/zhipu/glm-5.2?reasoning=xhigh', 'max'], +] as const)('Z.AI GLM-5.2: %s enables mapped reasoning effort', async (model, effort) => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' - const safety = new AbortController() - const safetyTimer = setTimeout(() => safety.abort(), 500) - const client = createOpenAIShimClient({}) as OpenAIShimClient - let caught: unknown - try { - await waitForPromise( - client.beta.messages.create( - { - model: 'gpt-5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }, - { signal: safety.signal }, - ), - 750, - 'GitHub codex responses timeout did not settle', + const expectedModel = model.split('?')[0]; + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: expectedModel, + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, ) - } catch (error) { - caught = error - } finally { - clearTimeout(safetyTimer) - } - - expect(caught).toBeDefined() - const error = caught as Error & { constructor: { name: string } } - expect(error.constructor.name).toBe('APIConnectionError') - expect(isOpenAIRequestNonReplayable(error)).toBe(true) - expect(fetchCalls).toBe(1) - expect(requestUrls).toEqual([ - 'https://api.githubcopilot.com/responses', - ]) -}) - -test('GitHub Copilot responses fallback does not replay after a pre-header timeout', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'test-token' - process.env.API_TIMEOUT_MS = '20' - const requestUrls: string[] = [] - - globalThis.fetch = (async (input, init) => { - const url = String(input) - requestUrls.push(url) - if (url.endsWith('/chat/completions')) { - return makeGithubChatFallbackResponse() - } - return pendingFetchUntilAbort(init) }) as unknown as FetchType - const safety = new AbortController() - const safetyTimer = setTimeout(() => safety.abort(), 500) const client = createOpenAIShimClient({}) as OpenAIShimClient - let caught: unknown - try { - await waitForPromise( - client.beta.messages.create( - { - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }, - { signal: safety.signal }, - ), - 750, - 'GitHub responses fallback timeout did not settle', - ) - } catch (error) { - caught = error - } finally { - clearTimeout(safetyTimer) - } + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) - expect(caught).toBeDefined() - const error = caught as Error & { constructor: { name: string } } - expect(error.constructor.name).toBe('APIConnectionError') - expect(isOpenAIRequestNonReplayable(error)).toBe(true) - expect(requestUrls).toEqual([ - 'https://api.githubcopilot.com/chat/completions', - 'https://api.githubcopilot.com/responses', - ]) + expect(requestBody?.model).toBe(expectedModel) + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe(effort) }) -test('GitHub Copilot responses fallback preserves caller abort without retrying', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'test-token' - process.env.API_TIMEOUT_MS = '200' - let fetchCalls = 0 +test.each([ + 'GLM-5.1?reasoning=high', + 'GLM-4.5-Air?reasoning=high', +] as const)('Z.AI GLM: %s does not receive GLM-5.2-only reasoning_effort', async model => { + process.env.OPENAI_BASE_URL = 'https://api.z.ai/api/coding/paas/v4' + process.env.OPENAI_API_KEY = 'sk-zai-test' - globalThis.fetch = (async (input, init) => { - fetchCalls++ - if (String(input).endsWith('/chat/completions')) { - return makeGithubChatFallbackResponse() - } - return pendingFetchUntilAbort(init) + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model, + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ) }) as unknown as FetchType - const caller = new AbortController() - const callerReason = new DOMException('Cancelled by user', 'AbortError') const client = createOpenAIShimClient({}) as OpenAIShimClient - const request = client.beta.messages.create( - { - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }, - { signal: caller.signal }, - ) - setTimeout(() => caller.abort(callerReason), 10) - - await expect( - waitForPromise(request, 500, 'GitHub responses fallback abort did not settle'), - ).rejects.toBe(callerReason) - expect(fetchCalls).toBe(2) -}) - -test('GitHub Copilot responses fallback preserves non-caller transport aborts', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'test-token' - let fetchCalls = 0 - const transportAbort = new DOMException('Proxy aborted request', 'AbortError') - - globalThis.fetch = (async input => { - fetchCalls++ - if (String(input).endsWith('/chat/completions')) { - return makeGithubChatFallbackResponse() - } - throw transportAbort - }) as unknown as FetchType + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + }) - const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toBe(transportAbort) - expect(fetchCalls).toBe(2) + expect(requestBody?.model).toBe(model.split('?', 1)[0]) + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBeUndefined() }) +// openaiShim test extraction seam 175 end -test('GitHub Copilot responses fallback does not retry non-retryable HTTP failures', async () => { - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'test-token' - let fetchCalls = 0 - globalThis.fetch = (async input => { - fetchCalls++ - if (String(input).endsWith('/chat/completions')) { - return makeGithubChatFallbackResponse() +test.each([ + ['non-streaming Z.AI request with tools', 'https://api.z.ai/api/coding/paas/v4', false, true, 'glm-5.2'], + ['streaming Z.AI request without tools', 'https://api.z.ai/api/coding/paas/v4', true, false, 'glm-5.2'], + ['streaming non-Z.AI request with tools', 'https://api.openai.com/v1', true, true, 'gpt-4o'], +] as const)('does not send tool_stream for %s', async (_name, baseUrl, stream, includeTools, model) => { + process.env.OPENAI_BASE_URL = baseUrl + process.env.OPENAI_API_KEY = 'sk-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + if (stream) { + return makeSseResponse(makeStreamChunks([ + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model, + choices: [{ index: 0, delta: { content: 'ok' }, finish_reason: null }], + }, + { + id: 'chatcmpl-1', + object: 'chat.completion.chunk', + model, + choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], + }, + ])) } return new Response( - JSON.stringify({ error: { message: 'invalid token' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, + JSON.stringify({ + id: 'chatcmpl-1', + model, + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, ) }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toMatchObject({ status: 401 }) - expect(fetchCalls).toBe(2) -}) - -// openaiShim test extraction seam 186 start: GitHub Copilot 401 chat_completions retries with refreshed token -test('GitHub Copilot 401 chat_completions retries with refreshed token', async () => { - const realModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'refreshed-token' - process.env.OPENAI_API_KEY = 'refreshed-token' - return true - }) - - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realModule, - refreshCopilotTokenOn401: refreshSpy, - })) - - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'initial-token' - process.env.GITHUB_TOKEN = 'initial-token' + await client.beta.messages.create({ + model, + messages: [{ role: 'user', content: 'hi' }], + tools: includeTools + ? [ + { + name: 'Bash', + description: 'Run a shell command', + input_schema: { + type: 'object', + properties: { command: { type: 'string' } }, + required: ['command'], + }, + }, + ] + : undefined, + max_tokens: 64, + stream, + }) - let fetchCallCount = 0 - let firstAuth: string | undefined - let secondAuth: string | undefined + expect(requestBody?.tool_stream).toBeUndefined() +}) +// openaiShim test extraction seam 176 end - globalThis.fetch = ((_input, init) => { - fetchCallCount++ - const headers = init?.headers as Record | undefined - const auth = headers?.Authorization - - if (fetchCallCount === 1) { - firstAuth = auth - return Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'token expired' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - ) - } - if (fetchCallCount === 2) { - secondAuth = auth - return Promise.resolve(makeChatCompletionResponse('gpt-4')) - } +// openaiShim test extraction seam 177 start: strips Anthropic attribution header block from chat-completions system prompt (#607) +test('strips Anthropic attribution header block from chat-completions system prompt (#607)', async () => { + let capturedBody: Record | undefined - throw new Error(`unexpected fetch call #${fetchCallCount}`) - }) as unknown as typeof globalThis.fetch + globalThis.fetch = (async (_input, init) => { + capturedBody = JSON.parse(String(init?.body)) as Record - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-retry') + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-4o', + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finish_reason: 'stop', + }, + ], + usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType - const client = createClient({}) as OpenAIShimClient + const client = createOpenAIShimClient({}) as OpenAIShimClient - const response = await client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) + await client.beta.messages.create({ + model: 'gpt-4o', + system: [ + { + type: 'text', + text: + 'x-anthropic-billing-header: cc_version=0.8.0.abc123; ' + + 'cc_entrypoint=cli;', + }, + { type: 'text', text: 'You are Claude Code, helpful assistant.' }, + { type: 'text', text: 'Project context: bun + react.' }, + ], + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) - expect(refreshSpy).toHaveBeenCalledTimes(1) - expect(process.env.GITHUB_TOKEN).toBe('refreshed-token') - expect(process.env.OPENAI_API_KEY).toBe('refreshed-token') - expect(fetchCallCount).toBe(2) - expect(firstAuth).toBe('Bearer initial-token') - expect(secondAuth).toBe('Bearer refreshed-token') - expect(response).toBeDefined() - } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realModule) - } + const messages = capturedBody?.messages as Array<{ role: string; content: string }> + const sysMsg = messages.find(m => m.role === 'system') + expect(sysMsg).toBeDefined() + expect(sysMsg?.content).not.toContain('x-anthropic-billing-header') + expect(sysMsg?.content).not.toContain('cc_version=') + expect(sysMsg?.content).toContain('You are Claude Code, helpful assistant.') + expect(sysMsg?.content).toContain('Project context: bun + react.') }) -// openaiShim test extraction seam 186 end - - -// openaiShim test extraction seam 187 start: GitHub Copilot 401 codex_responses retries with refreshed token -test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => { - const realGithubModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'refreshed-token' - process.env.OPENAI_API_KEY = 'refreshed-token' - return true - }) - - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) +// openaiShim test extraction seam 177 end - let codexCallCount = 0 - let firstAuth: string | undefined - let secondAuth: string | undefined - globalThis.fetch = ((_, init) => { - codexCallCount++ - const headers = new Headers(init?.headers) - const apiKey = headers.get('authorization')?.replace(/^Bearer /, '') +// openaiShim test extraction seam 178 start: strips Anthropic attribution header block from responses-API instructions (#607) +test('strips Anthropic attribution header block from responses-API instructions (#607)', async () => { + process.env.OPENAI_API_FORMAT = 'responses' + let capturedBody: Record | undefined - if (codexCallCount === 1) { - firstAuth = apiKey - return Promise.resolve(new Response(JSON.stringify({ error: { message: 'token expired' } }), { - status: 401, - headers: { 'Content-Type': 'application/json' }, - })) - } + globalThis.fetch = (async (_input, init) => { + capturedBody = JSON.parse(String(init?.body)) as Record - if (codexCallCount === 2) { - secondAuth = apiKey - return Promise.resolve(makeCodexSseResponse({ - response: { - id: 'resp_test', - output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }], - model: 'gpt-5', - usage: { input_tokens: 10, output_tokens: 5 }, + return new Response( + JSON.stringify({ + id: 'resp-1', + model: 'gpt-5.4', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'ok' }], }, - })) - } - - throw new Error(`unexpected codex call #${codexCallCount}`) - }) as unknown as FetchType - - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'initial-token' - process.env.GITHUB_TOKEN = 'initial-token' - - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-retry-codex') - - const client = createClient({}) as OpenAIShimClient + ], + usage: { input_tokens: 8, output_tokens: 3, total_tokens: 11 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType - const response = await client.beta.messages.create({ - model: 'gpt-5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) + const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient - expect(refreshSpy).toHaveBeenCalledTimes(1) - expect(process.env.GITHUB_TOKEN).toBe('refreshed-token') - expect(process.env.OPENAI_API_KEY).toBe('refreshed-token') - expect(codexCallCount).toBe(2) - expect(firstAuth).toBe('initial-token') - expect(secondAuth).toBe('refreshed-token') - expect(response).toBeDefined() - expect((response as Record).content).toBeDefined() - } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) - } + await client.beta.messages.create({ + model: 'gpt-5.4', + system: [ + { + type: 'text', + text: 'x-anthropic-billing-header: cc_version=0.8.0.abc123; cc_entrypoint=cli;', + }, + { type: 'text', text: 'You are Claude Code.' }, + ], + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 64, + stream: false, + }) + + const instructions = capturedBody?.instructions as string + expect(instructions).not.toContain('x-anthropic-billing-header') + expect(instructions).not.toContain('cc_version=') + expect(instructions).toContain('You are Claude Code.') }) -// openaiShim test extraction seam 187 end +// openaiShim test extraction seam 181 end -// openaiShim test extraction seam 188 start: GitHub Copilot 401 with credential pool uses refreshed token not pool key -test('GitHub Copilot 401 with credential pool uses refreshed token not pool key', async () => { - const realGithubModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'refreshed-token' - process.env.OPENAI_API_KEY = 'refreshed-token' - return true - }) +// openaiShim test extraction seam 182 start: DeepSeek: redacted_thinking block preserves continuity with reasoning_content: "" +test('DeepSeek: redacted_thinking block preserves continuity with reasoning_content: ""', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-chat', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - delete process.env.OPENAI_API_KEY - process.env.OPENAI_API_KEYS = 'initial-token,second-key' - process.env.GITHUB_TOKEN = 'initial-token' + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-chat', + system: 'test', + messages: [ + { role: 'user', content: 'analyze this' }, + { + role: 'assistant', + content: [ + // real redacted_thinking shape: content lives in `.data`, not `.thinking` + { type: 'redacted_thinking', data: '', signature: 'sig123' }, + { type: 'text', text: 'Analysis complete.' }, + { + type: 'tool_use', + id: 'call_redacted_1', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_redacted_1', content: 'files' }, + ], + }, + ], + max_tokens: 32, + stream: false, + }) - let fetchCallCount = 0 - let usedAuthHeaders: string[] = [] - - globalThis.fetch = ((_input, init) => { - fetchCallCount++ - const headers = init?.headers as Record | undefined - usedAuthHeaders.push(headers?.Authorization ?? '') - - if (fetchCallCount === 1) { - return Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'token expired' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - ) - } + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + // redacted_thinking is recognized as a thinking block; its .data is "" and the + // message carries a tool_call, so it falls back to reasoning_content: "" + expect(assistantWithToolCall?.reasoning_content).toBe('') +}) +// openaiShim test extraction seam 182 end - return Promise.resolve(makeChatCompletionResponse('gpt-4')) - }) as unknown as typeof globalThis.fetch - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-pool') +// openaiShim test extraction seam 183 start: DeepSeek: redacted_thinking block with non-empty data propagates data into reasoning_content +test('DeepSeek: redacted_thinking block with non-empty data propagates data into reasoning_content', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' - const client = createClient({}) as OpenAIShimClient + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-2', + model: 'deepseek-chat', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType - const response = await client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-chat', + system: 'test', + messages: [ + { role: 'user', content: 'analyze this' }, + { + role: 'assistant', + content: [ + // real redacted_thinking with content in .data + { + type: 'redacted_thinking', + data: 'encrypted_chain_of_thought_payload_v1', + signature: 'sig456', + }, + { type: 'text', text: 'Analysis complete.' }, + { + type: 'tool_use', + id: 'call_redacted_2', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_redacted_2', content: 'files' }, + ], + }, + ], + max_tokens: 32, + stream: false, + }) - expect(refreshSpy).toHaveBeenCalledTimes(1) - expect(fetchCallCount).toBe(2) - expect(usedAuthHeaders[0]).toBe('Bearer initial-token') - expect(usedAuthHeaders[1]).toBe('Bearer refreshed-token') - expect(response).toBeDefined() - } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) - } + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + // The real .data payload must be preserved in reasoning_content — this is the + // case the original test missed (it used a synthetic .thinking field). + expect(assistantWithToolCall?.reasoning_content).toBe( + 'encrypted_chain_of_thought_payload_v1', + ) }) -// openaiShim test extraction seam 188 end - - -// openaiShim test extraction seam 189 start: GitHub Copilot 401 with "token has expired" triggers refresh -test('GitHub Copilot 401 with "token has expired" triggers refresh', async () => { - const realGithubModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'refreshed-token' - process.env.OPENAI_API_KEY = 'refreshed-token' - return true - }) - - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) +// openaiShim test extraction seam 183 end - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'initial-token' - process.env.GITHUB_TOKEN = 'initial-token' - let fetchCallCount = 0 +// openaiShim test extraction seam 184 start: renders tool_reference blocks as text on the chat/completions path +test('renders tool_reference blocks as text on the chat/completions path', async () => { + const { __test } = await import('./openaiShim.ts') - globalThis.fetch = ((_input, init) => { - fetchCallCount++ + const messages = __test.convertMessages( + [ + { + role: 'assistant', + content: [ + { type: 'tool_use', id: 'call_ts1', name: 'ToolSearch', input: { query: 'memory' } }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_ts1', + content: [ + { type: 'tool_reference', tool_name: 'mcp__example__memory_search' }, + { type: 'tool_reference', tool_name: 'mcp__example__memory_store' }, + ], + }, + ], + }, + ], + undefined, + ) - if (fetchCallCount === 1) { - return Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'token has expired' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - ) - } + const toolMsg = messages.find(m => m.role === 'tool') + expect(toolMsg).toBeDefined() + // The rendering contract is plain text: text-only parts collapse to a string. + expect(typeof toolMsg!.content).toBe('string') + const content = toolMsg!.content as string + expect(content).toContain('mcp__example__memory_search') + expect(content).toContain('mcp__example__memory_store') +}) +// openaiShim test extraction seam 184 end - return Promise.resolve(makeChatCompletionResponse('gpt-4')) - }) as unknown as typeof globalThis.fetch - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-has-expired') +// openaiShim test extraction seam 185 start: preserves valid tool pairs after history pruning while dropping orphaned tool calls +test('preserves valid tool pairs after history pruning while dropping orphaned tool calls', async () => { + const { __test } = await import('./openaiShim.ts') - const client = createClient({}) as OpenAIShimClient + const messages = __test.convertMessages( + [ + { role: 'user', content: 'compacted summary of previous work' }, + { + role: 'assistant', + content: [ + { + type: 'tool_use', + id: 'call_pruned_without_result', + name: 'Read', + input: { file_path: 'old.ts' }, + }, + ], + }, + { role: 'user', content: 'continue with retained context' }, + { + role: 'assistant', + content: [ + { type: 'text', text: 'Reading the current file.' }, + { + type: 'tool_use', + id: 'call_retained', + name: 'Read', + input: { file_path: 'current.ts' }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_retained', + content: 'current contents', + }, + ], + }, + ], + undefined, + ) - const response = await client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }) + const toolCalls = messages.flatMap(message => message.tool_calls ?? []) + expect(toolCalls.map(toolCall => toolCall.id)).toEqual(['call_retained']) - expect(refreshSpy).toHaveBeenCalledTimes(1) - expect(fetchCallCount).toBe(2) - expect(response).toBeDefined() - } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) - } + const toolMessages = messages.filter(message => message.role === 'tool') + expect(toolMessages).toHaveLength(1) + expect(toolMessages[0]?.tool_call_id).toBe('call_retained') }) -// openaiShim test extraction seam 189 end - - -// openaiShim test extraction seam 190 start: GitHub Copilot 401 without expired-token message does not trigger refresh -test('GitHub Copilot 401 without expired-token message does not trigger refresh', async () => { - const realGithubModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => true) - - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) - - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'initial-token' - process.env.GITHUB_TOKEN = 'initial-token' - - let fetchCallCount = 0 +// openaiShim test extraction seam 185 end - globalThis.fetch = ((_input) => { - fetchCallCount++ - return Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'invalid token' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - ) - }) as unknown as typeof globalThis.fetch - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-no-refresh') +// Extraction boundary: history pruning | executor Copilot refresh behavior. +// The contiguous Copilot authentication retry block below moves with execution. +// Keep this marker stable for independent adjacent test migrations. +function makeCodexSseResponse(responseData: Record): Response { + const data = JSON.stringify(responseData) + return makeSseResponse([`event: response.completed\ndata: ${data}\n\n`]) +} - const client = createClient({}) as OpenAIShimClient +test('GitHub Copilot codex responses transport does not replay after a pre-header timeout', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'test-token' + process.env.API_TIMEOUT_MS = '20' + let fetchCalls = 0 + const requestUrls: string[] = [] - await expect( - client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() + globalThis.fetch = (async (input, init) => { + fetchCalls++ + requestUrls.push(String(input)) + return pendingFetchUntilAbort(init) + }) as unknown as FetchType - expect(refreshSpy).toHaveBeenCalledTimes(0) - expect(fetchCallCount).toBe(1) + const safety = new AbortController() + const safetyTimer = setTimeout(() => safety.abort(), 500) + const client = createOpenAIShimClient({}) as OpenAIShimClient + let caught: unknown + try { + await waitForPromise( + client.beta.messages.create( + { + model: 'gpt-5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }, + { signal: safety.signal }, + ), + 750, + 'GitHub codex responses timeout did not settle', + ) + } catch (error) { + caught = error } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) + clearTimeout(safetyTimer) } -}) -// openaiShim test extraction seam 190 end - - -// openaiShim test extraction seam 191 start: GitHub Copilot 401 refresh returning same token does not update auth -test('GitHub Copilot 401 refresh returning same token does not update auth', async () => { - const realGithubModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'initial-token' - process.env.OPENAI_API_KEY = 'initial-token' - return true - }) - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) + expect(caught).toBeDefined() + const error = caught as Error & { constructor: { name: string } } + expect(error.constructor.name).toBe('APIConnectionError') + expect(isOpenAIRequestNonReplayable(error)).toBe(true) + expect(fetchCalls).toBe(1) + expect(requestUrls).toEqual([ + 'https://api.githubcopilot.com/responses', + ]) +}) - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'initial-token' - process.env.GITHUB_TOKEN = 'initial-token' +test('GitHub Copilot responses fallback does not replay after a pre-header timeout', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'test-token' + process.env.API_TIMEOUT_MS = '20' + const requestUrls: string[] = [] - let fetchCallCount = 0 - let usedAuthHeaders: string[] = [] + globalThis.fetch = (async (input, init) => { + const url = String(input) + requestUrls.push(url) + if (url.endsWith('/chat/completions')) { + return makeGithubChatFallbackResponse() + } + return pendingFetchUntilAbort(init) + }) as unknown as FetchType - globalThis.fetch = ((_input, init) => { - fetchCallCount++ - const headers = init?.headers as Record | undefined - usedAuthHeaders.push(headers?.Authorization ?? '') + const safety = new AbortController() + const safetyTimer = setTimeout(() => safety.abort(), 500) + const client = createOpenAIShimClient({}) as OpenAIShimClient + let caught: unknown + try { + await waitForPromise( + client.beta.messages.create( + { + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }, + { signal: safety.signal }, + ), + 750, + 'GitHub responses fallback timeout did not settle', + ) + } catch (error) { + caught = error + } finally { + clearTimeout(safetyTimer) + } - return Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'token expired' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - ) - }) as unknown as typeof globalThis.fetch + expect(caught).toBeDefined() + const error = caught as Error & { constructor: { name: string } } + expect(error.constructor.name).toBe('APIConnectionError') + expect(isOpenAIRequestNonReplayable(error)).toBe(true) + expect(requestUrls).toEqual([ + 'https://api.githubcopilot.com/chat/completions', + 'https://api.githubcopilot.com/responses', + ]) +}) - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-same-token') +test('GitHub Copilot responses fallback preserves caller abort without retrying', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'test-token' + process.env.API_TIMEOUT_MS = '200' + let fetchCalls = 0 - const client = createClient({}) as OpenAIShimClient + globalThis.fetch = (async (input, init) => { + fetchCalls++ + if (String(input).endsWith('/chat/completions')) { + return makeGithubChatFallbackResponse() + } + return pendingFetchUntilAbort(init) + }) as unknown as FetchType - await expect( - client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() + const caller = new AbortController() + const callerReason = new DOMException('Cancelled by user', 'AbortError') + const client = createOpenAIShimClient({}) as OpenAIShimClient + const request = client.beta.messages.create( + { + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }, + { signal: caller.signal }, + ) + setTimeout(() => caller.abort(callerReason), 10) - expect(refreshSpy).toHaveBeenCalledTimes(1) - expect(fetchCallCount).toBeGreaterThanOrEqual(2) - expect(usedAuthHeaders.every(h => h === 'Bearer initial-token')).toBe(true) - } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) - } + await expect( + waitForPromise(request, 500, 'GitHub responses fallback abort did not settle'), + ).rejects.toBe(callerReason) + expect(fetchCalls).toBe(2) }) -// openaiShim test extraction seam 191 end - - -// openaiShim test extraction seam 192 start: GitHub Copilot 401 codex_responses with providerOverride does not trigger refresh -test('GitHub Copilot 401 codex_responses with providerOverride does not trigger refresh', async () => { - const realGithubModule = realGithubModelsCredentials - try { - const refreshSpy = mock(async () => { - process.env.GITHUB_TOKEN = 'refreshed-token' - process.env.OPENAI_API_KEY = 'refreshed-token' - return true - }) - mock.module('../../utils/githubModelsCredentials.js', () => ({ - ...realGithubModule, - refreshCopilotTokenOn401: refreshSpy, - })) +test('GitHub Copilot responses fallback preserves non-caller transport aborts', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'test-token' + let fetchCalls = 0 + const transportAbort = new DOMException('Proxy aborted request', 'AbortError') - process.env.CLAUDE_CODE_USE_GITHUB = '1' - process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'stored-copilot-token' - process.env.GITHUB_TOKEN = 'stored-copilot-token' - - // Mock fetch so performCodexRequest gets a 401 response (no codexShim mock needed) - globalThis.fetch = (() => - Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'token expired' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - )) as unknown as typeof globalThis.fetch + globalThis.fetch = (async input => { + fetchCalls++ + if (String(input).endsWith('/chat/completions')) { + return makeGithubChatFallbackResponse() + } + throw transportAbort + }) as unknown as FetchType - const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-override-codex') + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toBe(transportAbort) + expect(fetchCalls).toBe(2) +}) - // providerOverride.apiKey differs from OPENAI_API_KEY → credential source gate blocks refresh - const client = createClient({ - providerOverride: { model: 'gpt-5', baseURL: 'https://api.githubcopilot.com', apiKey: 'override-token' }, - }) as OpenAIShimClient +test('GitHub Copilot responses fallback does not retry non-retryable HTTP failures', async () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' + process.env.OPENAI_API_KEY = 'test-token' + let fetchCalls = 0 - await expect( - client.beta.messages.create({ - model: 'gpt-5', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() + globalThis.fetch = (async input => { + fetchCalls++ + if (String(input).endsWith('/chat/completions')) { + return makeGithubChatFallbackResponse() + } + return new Response( + JSON.stringify({ error: { message: 'invalid token' } }), + { status: 401, headers: { 'Content-Type': 'application/json' } }, + ) + }) as unknown as FetchType - expect(refreshSpy).toHaveBeenCalledTimes(0) - } finally { - mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) - } + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toMatchObject({ status: 401 }) + expect(fetchCalls).toBe(2) }) -// openaiShim test extraction seam 192 end +// openaiShim test extraction seam 186 end -// openaiShim test extraction seam 193 start: GitHub Copilot 401 chat_completions with providerOverride does not trigger refresh -test('GitHub Copilot 401 chat_completions with providerOverride does not trigger refresh', async () => { +// openaiShim test extraction seam 187 start: GitHub Copilot 401 codex_responses retries with refreshed token +test('GitHub Copilot 401 codex_responses retries with refreshed token', async () => { const realGithubModule = realGithubModelsCredentials try { const refreshSpy = mock(async () => { @@ -11464,37 +6103,63 @@ test('GitHub Copilot 401 chat_completions with providerOverride does not trigger refreshCopilotTokenOn401: refreshSpy, })) + let codexCallCount = 0 + let firstAuth: string | undefined + let secondAuth: string | undefined + + globalThis.fetch = ((_, init) => { + codexCallCount++ + const headers = new Headers(init?.headers) + const apiKey = headers.get('authorization')?.replace(/^Bearer /, '') + + if (codexCallCount === 1) { + firstAuth = apiKey + return Promise.resolve(new Response(JSON.stringify({ error: { message: 'token expired' } }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + })) + } + + if (codexCallCount === 2) { + secondAuth = apiKey + return Promise.resolve(makeCodexSseResponse({ + response: { + id: 'resp_test', + output: [{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] }], + model: 'gpt-5', + usage: { input_tokens: 10, output_tokens: 5 }, + }, + })) + } + + throw new Error(`unexpected codex call #${codexCallCount}`) + }) as unknown as FetchType + process.env.CLAUDE_CODE_USE_GITHUB = '1' process.env.OPENAI_BASE_URL = 'https://api.githubcopilot.com' - process.env.OPENAI_API_KEY = 'stored-copilot-token' - process.env.GITHUB_TOKEN = 'stored-copilot-token' - - globalThis.fetch = (() => - Promise.resolve( - new Response( - JSON.stringify({ error: { message: 'token expired' } }), - { status: 401, headers: { 'Content-Type': 'application/json' } }, - ), - )) as unknown as typeof globalThis.fetch + process.env.OPENAI_API_KEY = 'initial-token' + process.env.GITHUB_TOKEN = 'initial-token' const { createOpenAIShimClient: createClient } = - await importFreshOpenAIShim('copilot-401-override-chat') + await importFreshOpenAIShim('copilot-401-retry-codex') - // providerOverride.apiKey differs from OPENAI_API_KEY → credential source gate blocks refresh - const client = createClient({ - providerOverride: { model: 'gpt-4', baseURL: 'https://api.githubcopilot.com', apiKey: 'override-token' }, - }) as OpenAIShimClient + const client = createClient({}) as OpenAIShimClient - await expect( - client.beta.messages.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: 'hello' }], - max_tokens: 32, - stream: false, - }), - ).rejects.toThrow() + const response = await client.beta.messages.create({ + model: 'gpt-5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) - expect(refreshSpy).toHaveBeenCalledTimes(0) + expect(refreshSpy).toHaveBeenCalledTimes(1) + expect(process.env.GITHUB_TOKEN).toBe('refreshed-token') + expect(process.env.OPENAI_API_KEY).toBe('refreshed-token') + expect(codexCallCount).toBe(2) + expect(firstAuth).toBe('initial-token') + expect(secondAuth).toBe('refreshed-token') + expect(response).toBeDefined() + expect((response as Record).content).toBeDefined() } finally { mock.module('../../utils/githubModelsCredentials.js', () => realGithubModule) } @@ -11544,205 +6209,6 @@ async function collectFallbackEvents( globalThis.fetch = previousFetch } } - -// openaiShim test extraction seam 194 start: JSON fallback: preserves tool_calls as a tool_use block -test('JSON fallback: preserves tool_calls as a tool_use block', async () => { - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-tool', - model: 'fake-model', - choices: [ - { - message: { - role: 'assistant', - content: null, - tool_calls: [ - { - id: 'call_1', - type: 'function', - function: { name: 'Bash', arguments: '{"command":"pwd"}' }, - }, - ], - }, - finish_reason: 'tool_calls', - }, - ], - }) - - const toolStart = events.find( - event => - event.type === 'content_block_start' && - typeof event.content_block === 'object' && - event.content_block !== null && - (event.content_block as Record).type === 'tool_use', - ) as { content_block?: Record } | undefined - expect(toolStart?.content_block).toMatchObject({ - type: 'tool_use', - id: 'call_1', - name: 'Bash', - }) - - const inputDelta = events.find( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'input_json_delta', - ) as { delta?: { partial_json?: string } } | undefined - expect(JSON.parse(inputDelta?.delta?.partial_json ?? '{}')).toEqual({ - command: 'pwd', - }) - - const stopEvent = events.find(e => e.type === 'message_delta') as - | { delta?: { stop_reason?: string } } - | undefined - expect(stopEvent?.delta?.stop_reason).toBe('tool_use') -}) -// openaiShim test extraction seam 194 end - - -// openaiShim test extraction seam 195 start: JSON fallback: maps finish_reason=length to max_tokens -test('JSON fallback: maps finish_reason=length to max_tokens', async () => { - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-len', - model: 'fake-model', - choices: [ - { message: { role: 'assistant', content: 'partial' }, finish_reason: 'length' }, - ], - }) - const stopEvent = events.find(e => e.type === 'message_delta') as - | { delta?: { stop_reason?: string } } - | undefined - expect(stopEvent?.delta?.stop_reason).toBe('max_tokens') -}) -// openaiShim test extraction seam 195 end - - -// openaiShim test extraction seam 196 start: JSON fallback: preserves OpenCode Go quota error guidance -test('JSON fallback: preserves OpenCode Go quota error guidance', async () => { - process.env.OPENAI_BASE_URL = 'https://opencode.ai/zen/go/v1' - const previousFetch = globalThis.fetch - globalThis.fetch = (async () => - withResponseUrl( - makeJsonChatCompletion({ - error: { - type: 'FreeUsageLimitError', - message: 'free usage limit reached', - }, - }), - 'https://opencode.ai/zen/go/v1/chat/completions', - )) as unknown as FetchType - - try { - const client = createOpenAIShimClient({}) as OpenAIShimClient - const result = await client.beta.messages - .create({ - model: 'fake-model', - messages: [{ role: 'user', content: 'hi' }], - max_tokens: 64, - stream: true, - }) - .withResponse() - - let caught: unknown - try { - for await (const _event of result.data) { - // Consume until the JSON error is surfaced. - } - } catch (error) { - caught = error - } - - expect(caught).toBeInstanceOf(APIError) - const apiError = caught as APIError - expect(apiError.headers?.get('x-opencode-request-url')).toBe( - 'https://opencode.ai/zen/go/v1/chat/completions', - ) - const message = getAssistantMessageFromError(apiError, 'glm-5.1') - const first = message.message.content[0] - expect(typeof first === 'object' && first && 'text' in first ? first.text : '').toBe( - OPENCODE_GO_FREE_LIMIT_ERROR_MESSAGE, - ) - } finally { - globalThis.fetch = previousFetch - } -}) -// openaiShim test extraction seam 196 end - - -// openaiShim test extraction seam 197 start: JSON fallback: strips tags from emitted text -test('JSON fallback: strips tags from emitted text', async () => { - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-think', - model: 'fake-model', - choices: [ - { - message: { role: 'assistant', content: 'private planvisible answer' }, - finish_reason: 'stop', - }, - ], - }) - const textDelta = events.find( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'text_delta', - ) as { delta?: { text?: string } } | undefined - expect(textDelta?.delta?.text).toBe('visible answer') - expect(textDelta?.delta?.text).not.toContain('private plan') -}) -// openaiShim test extraction seam 197 end - - -// openaiShim test extraction seam 198 start: JSON fallback: normalizes array content into a text string -test('JSON fallback: normalizes array content into a text string', async () => { - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-array', - model: 'fake-model', - choices: [ - { - message: { - role: 'assistant', - content: [ - { type: 'text', text: 'line one' }, - { type: 'text', text: 'line two' }, - ], - }, - finish_reason: 'stop', - }, - ], - }) - const textDelta = events.find( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'text_delta', - ) as { delta?: { text?: unknown } } | undefined - expect(typeof textDelta?.delta?.text).toBe('string') - expect(textDelta?.delta?.text).toBe('line one\nline two') -}) -// openaiShim test extraction seam 198 end - - -// openaiShim test extraction seam 199 start: JSON fallback: recovers raw-text tool call into tool_use block - -// openaiShim test extraction seam 199 end - - -// openaiShim test extraction seam 200 start: JSON fallback façade terminates converted messages -test('JSON fallback façade terminates converted messages', async () => { - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-boundary', - model: 'boundary-model', - choices: [{ - message: { role: 'assistant', content: 'ok' }, - finish_reason: 'stop', - }], - }) - - expect(events.at(-1)?.type).toBe('message_stop') -}) // openaiShim test extraction seam 200 end // openaiShim test extraction seam 201 start: JSON fallback: recovers Tencent HY3 text tool calls into tool_use blocks @@ -11788,111 +6254,4 @@ test('JSON fallback: recovers Tencent HY3 text tool calls into tool_use blocks', | undefined expect(stopEvent?.delta?.stop_reason).toBe('tool_use') }) -// openaiShim test extraction seam 201 end - - -// openaiShim test extraction seam 202 start: JSON fallback: preserves HY3-looking text for non-Tencent model names -test('JSON fallback: preserves HY3-looking text for non-Tencent model names', async () => { - const text = - 'TaskCreate\nsubject: merely a documentation example\n' - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-non-tencent-hy3', - model: 'other/hy3-documentation', - choices: [ - { - message: { role: 'assistant', content: text }, - finish_reason: 'stop', - }, - ], - }, 'other/hy3-documentation') - const toolStart = events.find( - event => - event.type === 'content_block_start' && - typeof event.content_block === 'object' && - event.content_block !== null && - (event.content_block as Record).type === 'tool_use', - ) - const textDelta = events.find( - event => - event.type === 'content_block_delta' && - typeof event.delta === 'object' && - event.delta !== null && - (event.delta as Record).type === 'text_delta', - ) as { delta?: { text?: string } } | undefined - - expect(toolStart).toBeUndefined() - expect(textDelta?.delta?.text).toBe(text) -}) -// openaiShim test extraction seam 202 end - - -// openaiShim test extraction seam 203 start: JSON fallback: empty tool_calls array does not block raw-text recovery -test('JSON fallback: empty tool_calls array does not block raw-text recovery', async () => { - // tool_calls: [] is truthy; it must be treated as "no structured tool calls" - // so the raw "Tool calls requested" recovery still runs. - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-empty-tc', - model: 'fake-model', - choices: [ - { - message: { - role: 'assistant', - tool_calls: [], - content: - 'Tool calls requested:\n- Bash({"command":"ls"}) [id: call_empty_tc]', - }, - finish_reason: 'stop', - }, - ], - }) - const toolStart = events.find( - event => - event.type === 'content_block_start' && - typeof event.content_block === 'object' && - event.content_block !== null && - (event.content_block as Record).type === 'tool_use', - ) as { content_block?: Record } | undefined - expect(toolStart?.content_block).toMatchObject({ - type: 'tool_use', - id: 'call_empty_tc', - name: 'Bash', - }) -}) -// openaiShim test extraction seam 203 end - - -// openaiShim test extraction seam 204 start: JSON fallback: empty tool_calls does not block raw-text recovery on array content -test('JSON fallback: empty tool_calls does not block raw-text recovery on array content', async () => { - // Companion to the string-content case above: the array-content branch must - // also treat tool_calls: [] as "no structured tool calls" so raw recovery runs. - const events = await collectFallbackEvents({ - id: 'chatcmpl-json-empty-tc-array', - model: 'fake-model', - choices: [ - { - message: { - role: 'assistant', - tool_calls: [], - content: [ - { type: 'text', text: 'Tool calls requested:' }, - { type: 'text', text: '- Bash({"command":"ls"}) [id: call_empty_tc_arr]' }, - ], - }, - finish_reason: 'stop', - }, - ], - }) - const toolStart = events.find( - event => - event.type === 'content_block_start' && - typeof event.content_block === 'object' && - event.content_block !== null && - (event.content_block as Record).type === 'tool_use', - ) as { content_block?: Record } | undefined - expect(toolStart?.content_block).toMatchObject({ - type: 'tool_use', - id: 'call_empty_tc_arr', - name: 'Bash', - }) -}) // openaiShim test extraction seam 204 end From 7b494b2cd439cc623d9ed1055fc591be36df9ca5 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 13:31:38 -0700 Subject: [PATCH 19/25] =?UTF-8?q?docs(openai-shim):=20clarify=20extracted?= =?UTF-8?q?=20fa=C3=A7ade=20budget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/api/openaiShim/architecture.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/api/openaiShim/architecture.test.ts b/src/services/api/openaiShim/architecture.test.ts index 10199d0589..a57ed9a3be 100644 --- a/src/services/api/openaiShim/architecture.test.ts +++ b/src/services/api/openaiShim/architecture.test.ts @@ -5,9 +5,10 @@ import { describe, expect, test } from 'bun:test' const facadePath = fileURLToPath(new URL('../openaiShim.ts', import.meta.url)) const moduleDirectory = fileURLToPath(new URL('.', import.meta.url)) -// The rebased shared extraction seam is 5,636 lines. Each independently mergeable PR -// removes at least this many net lines when its representative module exists. -// Detecting modules makes the ceiling tighten regardless of merge order. +// The rebased shared extraction seam is 5,636 lines. The ten representative +// extractions below remove 4,054 net lines, yielding the verified 1,582-line +// fully extracted façade. Detecting modules makes the ceiling tighten regardless +// of merge order. const extractionDeltas = [ ['streamControl.ts', 169], ['providerCompatibility.ts', 115], From 8c3cb392bf773a7aad62dde4c04b81c5f2f8315f Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 13:36:45 -0700 Subject: [PATCH 20/25] test(openai-shim): enforce extraction modules --- src/services/api/openaiShim/architecture.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/api/openaiShim/architecture.test.ts b/src/services/api/openaiShim/architecture.test.ts index a57ed9a3be..e4decc59da 100644 --- a/src/services/api/openaiShim/architecture.test.ts +++ b/src/services/api/openaiShim/architecture.test.ts @@ -7,8 +7,7 @@ const moduleDirectory = fileURLToPath(new URL('.', import.meta.url)) // The rebased shared extraction seam is 5,636 lines. The ten representative // extractions below remove 4,054 net lines, yielding the verified 1,582-line -// fully extracted façade. Detecting modules makes the ceiling tighten regardless -// of merge order. +// fully extracted façade. const extractionDeltas = [ ['streamControl.ts', 169], ['providerCompatibility.ts', 115], @@ -24,9 +23,11 @@ const extractionDeltas = [ describe('openaiShim façade architecture', () => { test('does not regain logic removed by the independent extractions', () => { + for (const [moduleName] of extractionDeltas) { + expect(existsSync(`${moduleDirectory}/${moduleName}`)).toBe(true) + } const activeReduction = extractionDeltas.reduce( - (total, [moduleName, reduction]) => - total + (existsSync(`${moduleDirectory}/${moduleName}`) ? reduction : 0), + (total, [, reduction]) => total + reduction, 0, ) const facadeLines = readFileSync(facadePath, 'utf8').trimEnd().split('\n').length From fbebf7a7130e42d3d2e7460076dd21ecf360f7ce Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 13:44:08 -0700 Subject: [PATCH 21/25] fix(openai-shim): stop retrying cooled GitHub keys --- src/services/api/credentialPool.ts | 7 ++++ .../api/openaiShim/requestExecutor.test.ts | 30 ++++++++++++++++ .../api/openaiShim/requestExecutor.ts | 36 +++++++++++++++++-- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/services/api/credentialPool.ts b/src/services/api/credentialPool.ts index d818c0015e..9e2fe9c571 100644 --- a/src/services/api/credentialPool.ts +++ b/src/services/api/credentialPool.ts @@ -69,6 +69,13 @@ export class CredentialPool { return { value: fallback.value, index: leastRecentlyFailedIndex } } + hasAvailableCredential(): boolean { + const now = this.now() + return this.credentials.some( + credential => !credential.disabled && credential.cooldownUntil <= now, + ) + } + reportSuccess(lease: CredentialLease | null): void { if (!lease) return const credential = this.credentials[lease.index] diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index 025027305d..fc79c7df95 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -654,6 +654,36 @@ test('GitHub 429 cools the active pooled credential before retrying', async () = expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +test('GitHub 429 stops when every pooled credential is cooling down', async () => { + const authorizations: Array = [] + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://models.github.ai/inference' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await expect( + client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }), + ).rejects.toThrow('rate limited') + + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { const authorizations: Array = [] diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 1f7071367b..bb001212f0 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -852,7 +852,6 @@ export async function executeOpenAIRequest( } if (isGithub && response.status === 429 && attempt < maxAttempts - 1) { - await response.text().catch(() => {}) const delaySec = Math.min( GITHUB_429_BASE_DELAY_SEC * 2 ** attempt, GITHUB_429_MAX_DELAY_SEC, @@ -863,9 +862,40 @@ export async function executeOpenAIRequest( 'cooldown', CREDENTIAL_POOL_COOLDOWN_MS, ) + // Do not let CredentialPool.next() fall back to a cooled credential. + // If every pooled key is rate-limited, preserve this response as the + // final 429 rather than immediately retrying a key before its cooldown. + if (credentialPool.hasAvailableCredential()) { + await response.text().catch(() => {}) + await sleepMs(delaySec * 1000) + continue + } + const errorBody = await response.text().catch(() => 'unknown error') + let errorResponse: object | undefined + try { + errorResponse = JSON.parse(errorBody) + } catch { + /* raw text */ + } + throwClassifiedHttpError( + response.status, + errorBody, + errorResponse, + response.headers, + requestUrl, + formatRetryAfterHint(response), + classifyOpenAIHttpFailure({ + status: response.status, + body: errorBody, + url: requestUrl, + hasImages: bodyContainsImages(), + }), + ) + } else { + await response.text().catch(() => {}) + await sleepMs(delaySec * 1000) + continue } - await sleepMs(delaySec * 1000) - continue } // Read body exactly once here — Response body is a stream that can only // be consumed a single time. From 7e30882782c3ad89dfa5afc25beeef44da422268 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 14:55:35 -0700 Subject: [PATCH 22/25] fix(openai-shim): avoid concurrent cooled-key retries --- .../api/openaiShim/requestExecutor.test.ts | 44 +++++++++++++++++++ .../api/openaiShim/requestExecutor.ts | 11 +++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index fc79c7df95..875a58bbcc 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -684,6 +684,50 @@ test('GitHub 429 stops when every pooled credential is cooling down', async () = expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) }) +test('GitHub 429 does not retry a cooled key after a concurrent request cools the pool', async () => { + const authorizations: Array = [] + let markFirstRequestStarted: (() => void) | undefined + const firstRequestStarted = new Promise(resolve => { + markFirstRequestStarted = resolve + }) + + process.env.CLAUDE_CODE_USE_GITHUB = '1' + process.env.OPENAI_BASE_URL = 'https://models.github.ai/inference' + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + + globalThis.fetch = (async (_input, init) => { + const headers = init?.headers as Record | undefined + authorizations.push(headers?.Authorization ?? headers?.authorization ?? null) + if (authorizations.length === 1) { + markFirstRequestStarted?.() + } + return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + status: 429, + headers: { 'Content-Type': 'application/json' }, + }) + }) as unknown as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + const createRequest = () => client.beta.messages.create({ + model: 'gpt-5.5', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + const firstRequest = createRequest() + await firstRequestStarted + // Let the first request record key-a's cooldown and enter its backoff before + // the second request uses and cools key-b. + await new Promise(resolve => setTimeout(resolve, 20)) + const secondRequest = createRequest() + + await expect(secondRequest).rejects.toThrow('rate limited') + await expect(firstRequest).rejects.toThrow('rate limited') + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) +}) + test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { const authorizations: Array = [] diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index bb001212f0..33fde1adba 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -862,15 +862,20 @@ export async function executeOpenAIRequest( 'cooldown', CREDENTIAL_POOL_COOLDOWN_MS, ) + const errorBody = await response.text().catch(() => 'unknown error') // Do not let CredentialPool.next() fall back to a cooled credential. // If every pooled key is rate-limited, preserve this response as the // final 429 rather than immediately retrying a key before its cooldown. if (credentialPool.hasAvailableCredential()) { - await response.text().catch(() => {}) await sleepMs(delaySec * 1000) - continue + // Another concurrent request can cool the remaining credential while + // this request is backing off. Check again immediately before the + // synchronous next() call below would otherwise select its cooled-key + // fallback. + if (credentialPool.hasAvailableCredential()) { + continue + } } - const errorBody = await response.text().catch(() => 'unknown error') let errorResponse: object | undefined try { errorResponse = JSON.parse(errorBody) From b0d3846db98dee4b284f6fa29b9ad094ab6f5aa5 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 15:15:59 -0700 Subject: [PATCH 23/25] test(openai-shim): stabilize pooled-key retry coverage --- .../api/openaiShim/requestExecutor.test.ts | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/services/api/openaiShim/requestExecutor.test.ts b/src/services/api/openaiShim/requestExecutor.test.ts index 875a58bbcc..14cb81ec07 100644 --- a/src/services/api/openaiShim/requestExecutor.test.ts +++ b/src/services/api/openaiShim/requestExecutor.test.ts @@ -1,5 +1,5 @@ import { APIError } from '@anthropic-ai/sdk' -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, expect, jest, mock, test } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../../test/sharedMutationLock.js' import { asMockFetch } from '../../../test/typedMocks.js' import { _clearRegistryForTesting, ensureIntegrationsLoaded, registerGateway } from '../../../integrations/index.ts' @@ -482,6 +482,7 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + jest.useRealTimers() restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL) restoreEnv('OPENAI_API_BASE', originalEnv.OPENAI_API_BASE) restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY) @@ -690,6 +691,10 @@ test('GitHub 429 does not retry a cooled key after a concurrent request cools th const firstRequestStarted = new Promise(resolve => { markFirstRequestStarted = resolve }) + let markFirstResponseBodyRead: (() => void) | undefined + const firstResponseBodyRead = new Promise(resolve => { + markFirstResponseBodyRead = resolve + }) process.env.CLAUDE_CODE_USE_GITHUB = '1' process.env.OPENAI_BASE_URL = 'https://models.github.ai/inference' @@ -702,10 +707,17 @@ test('GitHub 429 does not retry a cooled key after a concurrent request cools th if (authorizations.length === 1) { markFirstRequestStarted?.() } - return new Response(JSON.stringify({ error: { message: 'rate limited' } }), { + return { + ok: false, status: 429, - headers: { 'Content-Type': 'application/json' }, - }) + headers: new Headers({ 'Content-Type': 'application/json' }), + text: () => { + if (authorizations.length === 1) { + markFirstResponseBodyRead?.() + } + return Promise.resolve(JSON.stringify({ error: { message: 'rate limited' } })) + }, + } as unknown as Response }) as unknown as FetchType const client = createOpenAIShimClient({}) as OpenAIShimClient @@ -716,16 +728,27 @@ test('GitHub 429 does not retry a cooled key after a concurrent request cools th stream: false, }) - const firstRequest = createRequest() - await firstRequestStarted - // Let the first request record key-a's cooldown and enter its backoff before - // the second request uses and cools key-b. - await new Promise(resolve => setTimeout(resolve, 20)) - const secondRequest = createRequest() + jest.useFakeTimers() + try { + const firstRequest = createRequest() + await firstRequestStarted + // The first body read happens after it cools key-a. Once it resolves, the + // request synchronously schedules its backoff in the next microtask. + await firstResponseBodyRead + for (let tick = 0; tick < 8 && jest.getTimerCount() === 0; tick++) { + await Promise.resolve() + } + expect(jest.getTimerCount()).toBe(1) - await expect(secondRequest).rejects.toThrow('rate limited') - await expect(firstRequest).rejects.toThrow('rate limited') - expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) + const secondRequest = createRequest() + await expect(secondRequest).rejects.toThrow('rate limited') + + jest.advanceTimersByTime(1_000) + await expect(firstRequest).rejects.toThrow('rate limited') + expect(authorizations).toEqual(['Bearer key-a', 'Bearer key-b']) + } finally { + jest.useRealTimers() + } }) test('OPENAI_API_KEYS does not reuse a cooled-down key after every key is rate-limited', async () => { From cf50221d224493bb93fc854e94b50d24ebe26517 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 15:45:24 -0700 Subject: [PATCH 24/25] fix(openai-shim): preserve newer credential cooldowns --- src/services/api/credentialPool.test.ts | 30 ++++++++++++++++++++++++- src/services/api/credentialPool.ts | 27 ++++++++++++++++++---- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/services/api/credentialPool.test.ts b/src/services/api/credentialPool.test.ts index 355b80fecb..67debb7554 100644 --- a/src/services/api/credentialPool.test.ts +++ b/src/services/api/credentialPool.test.ts @@ -45,6 +45,34 @@ test('CredentialPool cools down recoverable failures', () => { expect(pool.next()?.value).toBe('key-a') }) +test('CredentialPool does not let an older success clear a newer cooldown', () => { + const pool = new CredentialPool(['key-a', 'key-b']) + const firstA = pool.next() + const keyB = pool.next() + + pool.reportFailure(keyB, 'auth', 0) + const concurrentA = pool.next() + pool.reportFailure(concurrentA, 'cooldown', 30_000) + pool.reportSuccess(firstA) + + expect(pool.hasAvailableCredential()).toBe(false) +}) + +test('CredentialPool lets a stale auth failure permanently disable a cooled key', () => { + let now = 1_000 + const pool = new CredentialPool(['key-a', 'key-b'], () => now) + const firstA = pool.next() + const keyB = pool.next() + + pool.reportFailure(keyB, 'auth', 0) + const concurrentA = pool.next() + pool.reportFailure(concurrentA, 'cooldown', 30_000) + pool.reportFailure(firstA, 'auth', 0) + + now += 30_001 + expect(pool.hasAvailableCredential()).toBe(false) +}) + test('CredentialPool falls back to least-recently failed credential when all are cooling down', () => { let now = 1_000 const pool = new CredentialPool(['key-a', 'key-b'], () => now) @@ -56,5 +84,5 @@ test('CredentialPool falls back to least-recently failed credential when all are const second = pool.next() pool.reportFailure(second, 'cooldown', 30_000) - expect(pool.next()).toEqual({ value: 'key-a', index: 0 }) + expect(pool.next()).toEqual({ value: 'key-a', index: 0, generation: 1 }) }) diff --git a/src/services/api/credentialPool.ts b/src/services/api/credentialPool.ts index 9e2fe9c571..838edb3d80 100644 --- a/src/services/api/credentialPool.ts +++ b/src/services/api/credentialPool.ts @@ -3,6 +3,7 @@ export type CredentialPoolFailureKind = 'auth' | 'cooldown' export type CredentialLease = { value: string index: number + generation: number } type CredentialState = { @@ -10,6 +11,7 @@ type CredentialState = { disabled: boolean cooldownUntil: number lastFailureAt: number + generation: number } export class CredentialPool { @@ -23,6 +25,7 @@ export class CredentialPool { disabled: false, cooldownUntil: 0, lastFailureAt: 0, + generation: 0, })) this.now = now } @@ -44,7 +47,7 @@ export class CredentialPool { continue } this.cursor = (index + 1) % this.credentials.length - return { value: candidate.value, index } + return { value: candidate.value, index, generation: candidate.generation } } let leastRecentlyFailedIndex = -1 @@ -66,7 +69,11 @@ export class CredentialPool { const fallback = this.credentials[leastRecentlyFailedIndex] this.cursor = (leastRecentlyFailedIndex + 1) % this.credentials.length - return { value: fallback.value, index: leastRecentlyFailedIndex } + return { + value: fallback.value, + index: leastRecentlyFailedIndex, + generation: fallback.generation, + } } hasAvailableCredential(): boolean { @@ -79,7 +86,13 @@ export class CredentialPool { reportSuccess(lease: CredentialLease | null): void { if (!lease) return const credential = this.credentials[lease.index] - if (!credential || credential.value !== lease.value) return + if ( + !credential || + credential.value !== lease.value || + credential.generation !== lease.generation + ) { + return + } credential.cooldownUntil = 0 } @@ -90,9 +103,15 @@ export class CredentialPool { ): void { if (!lease) return const credential = this.credentials[lease.index] - if (!credential || credential.value !== lease.value) return + if (!credential || credential.value !== lease.value) { + return + } + if (kind !== 'auth' && credential.generation !== lease.generation) { + return + } const now = this.now() + credential.generation += 1 credential.lastFailureAt = now if (kind === 'auth') { credential.disabled = true From b548b49beeaf7b5c8767738d0338000a1e84c748 Mon Sep 17 00:00:00 2001 From: jatmn Date: Thu, 30 Jul 2026 16:05:28 -0700 Subject: [PATCH 25/25] docs(openai-shim): explain stale auth eviction --- src/services/api/credentialPool.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/api/credentialPool.ts b/src/services/api/credentialPool.ts index 838edb3d80..d83c967053 100644 --- a/src/services/api/credentialPool.ts +++ b/src/services/api/credentialPool.ts @@ -106,6 +106,7 @@ export class CredentialPool { if (!credential || credential.value !== lease.value) { return } + // Auth failures must evict a key even when a newer cooldown advanced its lease. if (kind !== 'auth' && credential.generation !== lease.generation) { return }