feat(ai): move API implementations to src/api with lazy wrappers (phase 2)

Stream implementations move from src/providers/ to src/api/, renamed by
API id (anthropic.ts -> anthropic-messages.ts, google.ts ->
google-generative-ai.ts, mistral.ts -> mistral-conversations.ts,
amazon-bedrock.ts -> bedrock-converse-stream.ts). Every module now
exports exactly stream/streamSimple; shared helpers move alongside.

New ProviderStreams dispatch contract in types.ts, lazyApi() wrapper in
api/lazy.ts, and one .lazy.ts wrapper per API. Bedrock's wrapper keeps
the node-only variable-specifier import and setBedrockProviderModule()
(now taking ProviderStreams).

providers/register-builtins.ts deleted; interim until the compat
entrypoint lands, builtin api-registry registration lives in stream.ts
and lazy wrappers are exported from the root barrel. Old per-API lazy
exports (streamAnthropic, ...) are gone; package.json subpaths retarget
to dist/api/.
This commit is contained in:
Mario Zechner
2026-06-10 20:08:59 +02:00
parent ee276e4db5
commit ba93da9a93
66 changed files with 283 additions and 560 deletions
@@ -2,7 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
import type { AddressInfo } from "node:net";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { streamAnthropic } from "../src/providers/anthropic.ts";
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
import type { Context, Model, Tool } from "../src/types.ts";
interface CapturedRequest {
@@ -1,8 +1,8 @@
import type Anthropic from "@anthropic-ai/sdk";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
import { getModel } from "../src/models.ts";
import { streamAnthropic } from "../src/providers/anthropic.ts";
import type { Context, ToolCall } from "../src/types.ts";
function createSseResponse(events: Array<{ event: string; data: string }>): Response {
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { stream as streamAzureOpenAIResponses } from "../src/api/azure-openai-responses.ts";
import { getModel } from "../src/models.ts";
import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.ts";
import type { Context } from "../src/types.ts";
interface CapturedAzureClientOptions {
@@ -44,8 +44,8 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => {
};
});
import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts";
import { getModel } from "../src/models.ts";
import { streamBedrock } from "../src/providers/amazon-bedrock.ts";
import type { Context, Message } from "../src/types.ts";
const baseModel = getModel("amazon-bedrock", "us.anthropic.claude-sonnet-4-5-20250929-v1:0");
@@ -51,9 +51,9 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => {
};
});
import type { BedrockOptions } from "../src/api/bedrock-converse-stream.ts";
import { stream as streamBedrock, streamSimple as streamSimpleBedrock } from "../src/api/bedrock-converse-stream.ts";
import { getModel } from "../src/models.ts";
import type { BedrockOptions } from "../src/providers/amazon-bedrock.ts";
import { streamBedrock, streamSimpleBedrock } from "../src/providers/amazon-bedrock.ts";
import type { Context, Model } from "../src/types.ts";
const context: Context = {
@@ -44,8 +44,8 @@ vi.mock("@aws-sdk/client-bedrock-runtime", () => {
};
});
import { stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts";
import { getModel } from "../src/models.ts";
import { streamBedrock } from "../src/providers/amazon-bedrock.ts";
import type { Context, Model } from "../src/types.ts";
const context: Context = {
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { type BedrockOptions, stream as streamBedrock } from "../src/api/bedrock-converse-stream.ts";
import { getModel } from "../src/models.ts";
import { type BedrockOptions, streamBedrock } from "../src/providers/amazon-bedrock.ts";
import type { Context, Model } from "../src/types.ts";
import { hasBedrockCredentials } from "./bedrock-utils.ts";
+3 -3
View File
@@ -1,8 +1,8 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts";
import { getModel } from "../src/models.ts";
import { streamAnthropic } from "../src/providers/anthropic.ts";
import { streamOpenAICompletions } from "../src/providers/openai-completions.ts";
import { streamOpenAIResponses } from "../src/providers/openai-responses.ts";
import { stream } from "../src/stream.ts";
import type { Context, Model } from "../src/types.ts";
@@ -10,13 +10,13 @@ import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { Type } from "typebox";
import { AuthStorage } from "../../coding-agent/src/core/auth-storage.ts";
import { getModel } from "../src/models.ts";
import {
closeOpenAICodexWebSocketSessions,
getOpenAICodexWebSocketDebugStats,
resetOpenAICodexWebSocketDebugStats,
streamOpenAICodexResponses,
} from "../src/providers/openai-codex-responses.ts";
stream as streamOpenAICodexResponses,
} from "../src/api/openai-codex-responses.ts";
import { getModel } from "../src/models.ts";
import type { AssistantMessage, Context, Message, Model, Tool, ToolResultMessage, Transport } from "../src/types.ts";
type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
+1 -1
View File
@@ -2,9 +2,9 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
import type { AddressInfo } from "node:net";
import { Type } from "typebox";
import { afterEach, describe, expect, it } from "vitest";
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts";
import { getModel, getModels } from "../src/models.ts";
import { streamAnthropic } from "../src/providers/anthropic.ts";
import type { Context, Model, Tool } from "../src/types.ts";
const originalFireworksApiKey = process.env.FIREWORKS_API_KEY;
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { stream as streamAnthropic } from "../src/api/anthropic-messages.ts";
import { getModel } from "../src/models.ts";
import { streamAnthropic } from "../src/providers/anthropic.ts";
import type { Context } from "../src/types.ts";
const mockState = vi.hoisted(() => ({
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { convertTools } from "../src/providers/google-shared.ts";
import { convertTools } from "../src/api/google-shared.ts";
import type { Tool } from "../src/types.ts";
function makeTool(parameters: Record<string, unknown>): Tool {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { convertMessages } from "../src/providers/google-shared.ts";
import { convertMessages } from "../src/api/google-shared.ts";
import type { Context, Model } from "../src/types.ts";
function makeGemini3Model<TApi extends "google-generative-ai" | "google-vertex">(
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { convertMessages } from "../src/providers/google-shared.ts";
import { convertMessages } from "../src/api/google-shared.ts";
import type { Context, Model } from "../src/types.ts";
function makeModel<TApi extends "google-generative-ai">(
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isThinkingPart, retainThoughtSignature } from "../src/providers/google-shared.ts";
import { isThinkingPart, retainThoughtSignature } from "../src/api/google-shared.ts";
describe("Google thinking detection (thoughtSignature)", () => {
it("treats part.thought === true as thinking", () => {
@@ -45,8 +45,8 @@ vi.mock("@google/genai", () => {
};
});
import { stream as streamGoogleVertex } from "../src/api/google-vertex.ts";
import { getModel } from "../src/models.ts";
import { streamGoogleVertex } from "../src/providers/google-vertex.ts";
import type { Context, Model } from "../src/types.ts";
const model = getModel("google-vertex", "gemini-3-flash-preview");
+2 -2
View File
@@ -66,7 +66,7 @@ describe("lazy provider module loading", () => {
expect(result.loadedSpecifiers).toEqual([]);
});
it("loads only the Anthropic SDK when calling the root lazy wrapper", () => {
it("loads only the Anthropic SDK when streaming through the lazy API wrapper", () => {
const result = runProbe(`
const model = {
id: "claude-sonnet-4-6",
@@ -81,7 +81,7 @@ describe("lazy provider module loading", () => {
maxTokens: 8192,
};
const context = { messages: [{ role: "user", content: "hi" }] };
await mod.streamSimpleAnthropic(model, context).result();
await mod.anthropicMessagesApi().streamSimple(model, context).result();
`);
expect(result.loadedSpecifiers).toEqual(["@anthropic-ai/sdk"]);
+3 -3
View File
@@ -5,9 +5,9 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import {
getOpenAICodexWebSocketDebugStats,
resetOpenAICodexWebSocketDebugStats,
streamOpenAICodexResponses,
streamSimpleOpenAICodexResponses,
} from "../src/providers/openai-codex-responses.ts";
stream as streamOpenAICodexResponses,
streamSimple as streamSimpleOpenAICodexResponses,
} from "../src/api/openai-codex-responses.ts";
import type { Context, Model } from "../src/types.ts";
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
@@ -1,7 +1,7 @@
import { Type } from "typebox";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import { getModel } from "../src/models.ts";
import { streamOpenAICompletions } from "../src/providers/openai-completions.ts";
import type { Model } from "../src/types.ts";
interface CacheControl {
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import { getModel } from "../src/models.ts";
import { streamOpenAICompletions } from "../src/providers/openai-completions.ts";
import type { Model } from "../src/types.ts";
interface FakeOpenAIClientOptions {
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { streamOpenAICompletions } from "../src/providers/openai-completions.ts";
import { stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import type { Context, Model } from "../src/types.ts";
const mockState = vi.hoisted(() => ({
@@ -2,7 +2,7 @@ import { once } from "node:events";
import http from "node:http";
import type { AddressInfo } from "node:net";
import { afterEach, describe, expect, it } from "vitest";
import { convertMessages, streamOpenAICompletions } from "../src/providers/openai-completions.ts";
import { convertMessages, stream as streamOpenAICompletions } from "../src/api/openai-completions.ts";
import type {
AssistantMessage,
AssistantMessageEvent,
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { convertMessages } from "../src/api/openai-completions.ts";
import { getModel } from "../src/models.ts";
import { convertMessages } from "../src/providers/openai-completions.ts";
import type {
AssistantMessage,
Context,
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { stream as streamOpenAIResponses } from "../src/api/openai-responses.ts";
import { getModel } from "../src/models.ts";
import { streamOpenAIResponses } from "../src/providers/openai-responses.ts";
import type { Model } from "../src/types.ts";
type CapturedHeaders = Headers | string[][] | Record<string, string | readonly string[]> | undefined;
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts";
import { getModel } from "../src/models.ts";
import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts";
import type { AssistantMessage, Context, ToolResultMessage, Usage } from "../src/types.ts";
import { shortHash } from "../src/utils/hash.ts";
@@ -1,7 +1,7 @@
import type { ResponseOutputMessage } from "openai/resources/responses/responses.js";
import { describe, expect, it } from "vitest";
import { convertResponsesMessages } from "../src/api/openai-responses-shared.ts";
import { getModel } from "../src/models.ts";
import { convertResponsesMessages } from "../src/providers/openai-responses-shared.ts";
import type { AssistantMessage, Context, Usage } from "../src/types.ts";
const usage: Usage = {
@@ -1,6 +1,6 @@
import type { ResponseStreamEvent } from "openai/resources/responses/responses.js";
import { describe, expect, it, vi } from "vitest";
import { processResponsesStream } from "../src/providers/openai-responses-shared.ts";
import { processResponsesStream } from "../src/api/openai-responses-shared.ts";
import type { AssistantMessage, AssistantMessageEvent, Model } from "../src/types.ts";
import { AssistantMessageEventStream } from "../src/utils/event-stream.ts";
+5 -3
View File
@@ -2,10 +2,12 @@
// Run from packages/ai: node test/scratch.ts
// Requires ANTHROPIC_API_KEY.
import { anthropicMessagesApi } from "../src/api/anthropic-messages.lazy.ts";
import { createModels, getModels, type Provider } from "../src/models.ts";
import { streamAnthropic, streamSimpleAnthropic } from "../src/providers/register-builtins.ts";
import type { Context } from "../src/types.ts";
const anthropicApi = anthropicMessagesApi();
// ---------------------------------------------------------------------------
// 1. Define a provider. In the final design this comes from
// `@earendil-works/pi-ai/providers/anthropic` as `anthropicProvider()`;
@@ -33,8 +35,8 @@ const anthropic: Provider<"anthropic-messages"> = {
getModels: async () => getModels("anthropic"),
// shared lazy API implementation (loads the SDK on first request)
stream: streamAnthropic,
streamSimple: streamSimpleAnthropic,
stream: anthropicApi.stream,
streamSimple: anthropicApi.streamSimple,
};
// ---------------------------------------------------------------------------
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { transformMessages } from "../src/providers/transform-messages.ts";
import { transformMessages } from "../src/api/transform-messages.ts";
import type { AssistantMessage, Message, Model, ToolCall } from "../src/types.ts";
// Normalize function matching what anthropic.ts uses