Skip to content
1 change: 1 addition & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

### Added

- Added an optional request-scoped `fetch` transport to OpenAI-compatible Completions so governed providers can enforce their own connection pinning, deadlines, body bounds, and retry policy without bypassing the shared response parser.
- Refreshed generated model catalogs from models.dev, adding newly listed models including Kimi K2.7 Code for GitHub Copilot and Fable 5 to several providers ([#6256](https://github.com/earendil-works/pi/issues/6256)).
- Added Claude Sonnet 5 to the GitHub Copilot model catalog ([#6200](https://github.com/earendil-works/pi/issues/6200)).
- Added zstd request-body compression for the OpenAI Codex Responses SSE transport. Requests are sent with `Content-Encoding: zstd` when Node/Bun zstd support is available; the WebSocket transport is unchanged.
Expand Down
19 changes: 15 additions & 4 deletions packages/ai/src/api/openai-completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ function isEncryptedReasoningDetail(detail: unknown): detail is OpenAIEncryptedR
export interface OpenAICompletionsOptions extends StreamOptions {
toolChoice?: "auto" | "none" | "required" | { type: "function"; function: { name: string } };
reasoningEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
/** Provider-scoped transport override for endpoint pinning and bounded response streams. */
fetch?: typeof globalThis.fetch;
}

export interface OpenAICompletionsSimpleOptions extends SimpleStreamOptions {
toolChoice?: OpenAICompletionsOptions["toolChoice"];
/** Provider-scoped transport override for endpoint pinning and bounded response streams. */
fetch?: typeof globalThis.fetch;
}

interface OpenAICompatCacheControl {
Expand Down Expand Up @@ -180,7 +188,7 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio
const compat = getCompat(model);
const cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat);
const client = createClient(model, context, apiKey, options?.headers, cacheSessionId, compat, options?.fetch);
let params = buildParams(model, context, options, compat, cacheRetention);
const nextParams = await options?.onPayload?.(params, model);
if (nextParams !== undefined) {
Expand Down Expand Up @@ -487,22 +495,23 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio
return stream;
};

export const streamSimple: StreamFunction<"openai-completions", SimpleStreamOptions> = (
export const streamSimple: StreamFunction<"openai-completions", OpenAICompletionsSimpleOptions> = (
model: Model<"openai-completions">,
context: Context,
options?: SimpleStreamOptions,
options?: OpenAICompletionsSimpleOptions,
): AssistantMessageEventStream => {
getClientApiKey(model.provider, options?.apiKey, options?.headers);

const base = buildBaseOptions(model, context, options, options?.apiKey);
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
const toolChoice = (options as OpenAICompletionsOptions | undefined)?.toolChoice;
const toolChoice = options?.toolChoice;

return stream(model, context, {
...base,
reasoningEffort,
toolChoice,
fetch: options?.fetch,
} satisfies OpenAICompletionsOptions);
};

Expand All @@ -513,6 +522,7 @@ function createClient(
optionsHeaders?: ProviderHeaders,
sessionId?: string,
compat: ResolvedOpenAICompletionsCompat = getCompat(model),
requestFetch?: typeof globalThis.fetch,
) {
const headers: ProviderHeaders = { ...model.headers };
if (model.provider === "github-copilot") {
Expand Down Expand Up @@ -540,6 +550,7 @@ function createClient(
baseURL: model.baseUrl,
dangerouslyAllowBrowser: true,
defaultHeaders: headers,
fetch: requestFetch,
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type { GoogleVertexOptions } from "./api/google-vertex.ts";
export * from "./api/lazy.ts";
export type { MistralOptions } from "./api/mistral-conversations.ts";
export type { OpenAICodexResponsesOptions, OpenAICodexWebSocketDebugStats } from "./api/openai-codex-responses.ts";
export type { OpenAICompletionsOptions } from "./api/openai-completions.ts";
export type { OpenAICompletionsOptions, OpenAICompletionsSimpleOptions } from "./api/openai-completions.ts";
export type { OpenAIResponsesOptions } from "./api/openai-responses.ts";
export * from "./auth/context.ts";
export * from "./auth/credential-store.ts";
Expand Down
4 changes: 2 additions & 2 deletions packages/ai/src/legacy-api-aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { MistralOptions } from "./api/mistral-conversations.ts";
import { openAICodexResponsesApi } from "./api/openai-codex-responses.lazy.ts";
import type { OpenAICodexResponsesOptions } from "./api/openai-codex-responses.ts";
import { openAICompletionsApi } from "./api/openai-completions.lazy.ts";
import type { OpenAICompletionsOptions } from "./api/openai-completions.ts";
import type { OpenAICompletionsOptions, OpenAICompletionsSimpleOptions } from "./api/openai-completions.ts";
import { openAIResponsesApi } from "./api/openai-responses.lazy.ts";
import type { OpenAIResponsesOptions } from "./api/openai-responses.ts";
import type { SimpleStreamOptions, StreamFunction } from "./types.ts";
Expand Down Expand Up @@ -93,7 +93,7 @@ export const streamOpenAICompletions = openAICompletionsStreams.stream as Stream
/** @deprecated Use `streamSimple` from `@earendil-works/pi-ai/api/openai-completions` or `openAICompletionsApi().streamSimple`. */
export const streamSimpleOpenAICompletions = openAICompletionsStreams.streamSimple as StreamFunction<
"openai-completions",
SimpleStreamOptions
OpenAICompletionsSimpleOptions
>;

/** @deprecated Use `stream` from `@earendil-works/pi-ai/api/openai-responses` or `openAIResponsesApi().stream`. */
Expand Down
16 changes: 16 additions & 0 deletions packages/ai/test/openai-completions-empty-tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { streamSimple as streamSimpleOpenAICompletions } from "../src/api/openai-completions.ts";
import { getModel, streamSimple } from "../src/compat.ts";

// Empty tools arrays must NOT be serialized as `tools: []` — some OpenAI-compatible
Expand Down Expand Up @@ -92,6 +93,21 @@ describe("openai-completions empty tools handling", () => {
expect("tools" in (params as object)).toBe(false);
});

it("passes a request-scoped fetch transport to the OpenAI client", async () => {
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
const model = { ...baseModel, api: "openai-completions" } as const;
const requestFetch = vi.fn() as unknown as typeof globalThis.fetch;

await streamSimpleOpenAICompletions(
model,
{ messages: [{ role: "user", content: "hi", timestamp: Date.now() }] },
{ apiKey: "test", fetch: requestFetch },
).result();

const clientOptions = mockState.lastClientOptions as { fetch?: typeof globalThis.fetch };
expect(clientOptions.fetch).toBe(requestFetch);
});

it("sends default maxTokens", async () => {
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
const model = { ...baseModel, api: "openai-completions" } as const;
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Added public SDK exports for CLI-equivalent model and scoped-model resolution ([#6201](https://github.com/earendil-works/pi/issues/6201)).
- Added extension entry renderers for persisted display-only session entries that are rendered in interactive mode without being sent to the model context.
- Added optional terminal and process-handler injection to `InteractiveMode` for deterministic embedded and UI-test environments.
- Added a fail-closed Vinci Worker lane for the exact non-authoritative `Qwen/Qwen3.8-27B` H200 endpoint, gated by independently signed exact-build qualification, bounded pinned transport, per-attempt accounting, and the 1→2→4→8→16→24→32 burn-in schema (runtime concurrency remains 1 until a fleet permit authority exists).

### Changed

Expand Down
12 changes: 12 additions & 0 deletions vinci/bin/vinci
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,18 @@ case "${VINCI_PROVIDER}" in
}
export VINCI_DEEPINFRA_QUALIFICATION=1
;;
qwen-h200)
[ "${VINCI_QWEN_SELECTED:-0}" = "1" ] || {
echo "✗ qwen-h200 is a Worker-only qualified provider" >&2
exit 2
}
[ "${VINCI_MODEL}" = "Qwen/Qwen3.8-27B" ] || {
echo "✗ The Qwen H200 lane is pinned to Qwen/Qwen3.8-27B" >&2
exit 2
}
VINCI_QWEN_EXTENSION="${VINCI}/extensions/vinci-qwen-provider.ts"
set -- --extension "${VINCI_QWEN_EXTENSION}" "$@"
;;
*)
# Offline process-level regressions inject a faux provider without reopening public CLI flags.
[ "${VINCI_INTERNAL_PROVIDER_TEST:-0}" = "1" ] || {
Expand Down
Loading