fix(ai): upgrade openai SDK to 7.19.0 (#10044)

* fix(ai): upgrade openai SDK to 7.19.0

Adds the "fast" service tier to the SDK types, needed to price
GPT-6 Fast mode requests correctly. Drops the local
prompt_cache_options type, which the SDK now defines.

* fix(coding-agent): check Cloudflare compat request via fetch instead of SDK mock

pi-ai now resolves its own nested openai 7.x while evals keeps 6.x at the
root, so vi.mock("openai") in coding-agent no longer reached pi-ai's client.
Capture the outgoing request with a fake fetch and assert the URL and headers.

* fix(ai): drop any casts for stream_options and max_tokens in openai-completions

stream_options is typed by the SDK, so assign it directly. max_tokens is
deprecated by OpenAI but still needed for OpenAI-compatible providers
that reject max_completion_tokens; use a narrow cast instead of any to
avoid the deprecation warning.
This commit is contained in:
David Brailovsky
2026-09-25 17:27:12 +02:00
committed by GitHub
parent ff72faba28
commit ab30693d64
7 changed files with 161 additions and 98 deletions
+39 -1
View File
@@ -4276,6 +4276,7 @@
"version": "6.40.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz",
"integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==",
"dev": true,
"license": "Apache-2.0",
"peerDependencies": {
"ws": "^8.18.0",
@@ -5542,7 +5543,7 @@
"@smithy/node-http-handler": "4.12.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"openai": "6.40.0",
"openai": "7.19.0",
"partial-json": "0.1.7",
"typebox": "1.3.27"
},
@@ -5581,6 +5582,43 @@
"node": ">= 20"
}
},
"packages/ai/node_modules/openai": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-7.19.0.tgz",
"integrity": "sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.0.0"
},
"peerDependencies": {
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
"@smithy/hash-node": ">=4.3.0 <5",
"@smithy/signature-v4": ">=5.4.0 <6",
"undici": ">=5 <9",
"ws": "^8.21.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-provider-node": {
"optional": true
},
"@smithy/hash-node": {
"optional": true
},
"@smithy/signature-v4": {
"optional": true
},
"undici": {
"optional": true
},
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"packages/chord": {
"name": "@earendil-works/chord",
"version": "0.87.1",
+1 -1
View File
@@ -70,7 +70,7 @@
"@smithy/node-http-handler": "4.12.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"openai": "6.40.0",
"openai": "7.19.0",
"partial-json": "0.1.7",
"typebox": "1.3.27"
},
+3 -2
View File
@@ -827,7 +827,7 @@ function buildParams(
};
if (compat.supportsUsageInStreaming !== false) {
(params as any).stream_options = { include_usage: true };
params.stream_options = { include_usage: true };
}
if (compat.supportsStore) {
@@ -836,7 +836,8 @@ function buildParams(
if (options?.maxTokens) {
if (compat.maxTokensField === "max_tokens") {
(params as any).max_tokens = options.maxTokens;
// Deprecated by OpenAI, but some OpenAI-compatible providers only accept max_tokens.
(params as { max_tokens?: number }).max_tokens = options.maxTokens;
} else {
params.max_completion_tokens = options.maxTokens;
}
+2 -4
View File
@@ -92,7 +92,7 @@ function getPromptCacheRetention(
function getPromptCacheOptions(
compat: Required<OpenAIResponsesCompat>,
cacheRetention: CacheRetention,
): { mode?: "explicit"; ttl?: "30m" } | undefined {
): ResponseCreateParamsStreaming["prompt_cache_options"] {
if (!compat.supportsExplicitPromptCacheMode) return undefined;
if (cacheRetention === "none") return { mode: "explicit" };
if (cacheRetention === "long" && compat.supportsLongCacheRetention) return { ttl: "30m" };
@@ -307,9 +307,7 @@ function buildParams(
});
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
const params: ResponseCreateParamsStreaming & {
prompt_cache_options?: { mode?: "explicit"; ttl?: "30m" };
} = {
const params: ResponseCreateParamsStreaming = {
model: model.id,
input: messages,
stream: true,
+38 -19
View File
@@ -425,7 +425,7 @@
"@smithy/node-http-handler": "4.12.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"openai": "6.40.0",
"openai": "7.19.0",
"partial-json": "0.1.7",
"typebox": "1.3.27"
},
@@ -459,6 +459,43 @@
"node": ">= 20"
}
},
"node_modules/@earendil-works/pi-ai/node_modules/openai": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-7.19.0.tgz",
"integrity": "sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
"@smithy/hash-node": ">=4.3.0 <5",
"@smithy/signature-v4": ">=5.4.0 <6",
"undici": ">=5 <9",
"ws": "^8.21.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-provider-node": {
"optional": true
},
"@smithy/hash-node": {
"optional": true
},
"@smithy/signature-v4": {
"optional": true
},
"undici": {
"optional": true
},
"ws": {
"optional": true
},
"zod": {
"optional": true
}
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@earendil-works/pi-coding-agent": {
"version": "0.87.1",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.87.1.tgz",
@@ -1630,24 +1667,6 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/openai": {
"version": "6.40.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz",
"integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==",
"license": "Apache-2.0",
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
+38 -19
View File
@@ -447,7 +447,7 @@
"@smithy/node-http-handler": "4.12.1",
"http-proxy-agent": "9.1.0",
"https-proxy-agent": "9.1.0",
"openai": "6.40.0",
"openai": "7.19.0",
"partial-json": "0.1.7",
"typebox": "1.3.27"
},
@@ -481,6 +481,43 @@
"node": ">= 20"
}
},
"node_modules/@earendil-works/pi-ai/node_modules/openai": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-7.19.0.tgz",
"integrity": "sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
"@smithy/hash-node": ">=4.3.0 <5",
"@smithy/signature-v4": ">=5.4.0 <6",
"undici": ">=5 <9",
"ws": "^8.21.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-provider-node": {
"optional": true
},
"@smithy/hash-node": {
"optional": true
},
"@smithy/signature-v4": {
"optional": true
},
"undici": {
"optional": true
},
"ws": {
"optional": true
},
"zod": {
"optional": true
}
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@earendil-works/pi-telemetry": {
"version": "0.87.1",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.87.1.tgz",
@@ -1620,24 +1657,6 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/openai": {
"version": "6.40.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz",
"integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==",
"license": "Apache-2.0",
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
@@ -1,46 +1,34 @@
import { complete, resetApiProviders } from "@earendil-works/pi-ai/compat";
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { ModelRegistry } from "../src/core/model-registry.ts";
import { ModelRuntime } from "../src/core/model-runtime.ts";
const openAIState = vi.hoisted(() => ({ clientOptions: undefined as unknown }));
interface CapturedRequest {
url: string;
headers: Headers;
}
vi.mock("openai", () => {
class FakeOpenAI {
constructor(options: unknown) {
openAIState.clientOptions = options;
}
chat = {
completions: {
create: () => {
const stream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [{ delta: {}, finish_reason: "stop" }],
usage: { prompt_tokens: 1, completion_tokens: 1 },
};
},
};
const promise = Promise.resolve(stream) as Promise<typeof stream> & {
withResponse(): Promise<{
data: typeof stream;
response: { status: number; headers: Headers };
}>;
};
promise.withResponse = async () => ({
data: stream,
response: { status: 200, headers: new Headers() },
});
return promise;
},
},
function createCapturingFetch(): { fetch: typeof globalThis.fetch; requests: CapturedRequest[] } {
const requests: CapturedRequest[] = [];
const fetch: typeof globalThis.fetch = async (input, init) => {
const request = new Request(input, init);
requests.push({ url: request.url, headers: request.headers });
const chunk = {
id: "chatcmpl-test",
object: "chat.completion.chunk",
created: 0,
model: "test",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
};
}
return { default: FakeOpenAI };
});
return new Response(`data: ${JSON.stringify(chunk)}\n\ndata: [DONE]\n\n`, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
};
return { fetch, requests };
}
async function createCloudflareRuntime(): Promise<{ modelRuntime: ModelRuntime; modelRegistry: ModelRegistry }> {
const authStorage = AuthStorage.inMemory();
@@ -56,6 +44,8 @@ async function createCloudflareRuntime(): Promise<{ modelRuntime: ModelRuntime;
return { modelRuntime, modelRegistry: new ModelRegistry(modelRuntime) };
}
const CLOUDFLARE_COMPAT_URL = "https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat/chat/completions";
describe("ModelRegistry Cloudflare compat streaming", () => {
it("materializes the Cloudflare endpoint through ModelRuntime streaming", async () => {
const { modelRuntime } = await createCloudflareRuntime();
@@ -63,14 +53,13 @@ describe("ModelRegistry Cloudflare compat streaming", () => {
expect(model).toBeDefined();
resetApiProviders();
await modelRuntime.completeSimple(model!, { messages: [] });
const { fetch, requests } = createCapturingFetch();
const result = await modelRuntime.completeSimple(model!, { messages: [] }, { fetch });
const clientOptions = openAIState.clientOptions as {
baseURL?: string;
defaultHeaders?: Record<string, unknown>;
};
expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat");
expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test-token");
expect(result.stopReason).toBe("stop");
expect(requests).toHaveLength(1);
expect(requests[0].url).toBe(CLOUDFLARE_COMPAT_URL);
expect(requests[0].headers.get("cf-aig-authorization")).toBe("Bearer test-token");
});
it("materializes the Cloudflare endpoint after extension-style auth resolution", async () => {
@@ -88,15 +77,14 @@ describe("ModelRegistry Cloudflare compat streaming", () => {
"x-api-key": null,
});
await complete(model!, { messages: [] }, auth);
const { fetch, requests } = createCapturingFetch();
const result = await complete(model!, { messages: [] }, { ...auth, fetch });
const clientOptions = openAIState.clientOptions as {
baseURL?: string;
defaultHeaders?: Record<string, unknown>;
};
expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/test-account/test-gateway/compat");
expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test-token");
expect(clientOptions.defaultHeaders?.Authorization).toBeNull();
expect(clientOptions.defaultHeaders?.["x-api-key"]).toBeNull();
expect(result.stopReason).toBe("stop");
expect(requests).toHaveLength(1);
expect(requests[0].url).toBe(CLOUDFLARE_COMPAT_URL);
expect(requests[0].headers.get("cf-aig-authorization")).toBe("Bearer test-token");
expect(requests[0].headers.has("authorization")).toBe(false);
expect(requests[0].headers.has("x-api-key")).toBe(false);
});
});