@xiaozhiclaw/provider-core-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export declare function isAuthorized(req: IncomingMessage, serviceToken?: string): boolean;
package/dist/auth.js ADDED
@@ -0,0 +1,9 @@
1
+ export function isAuthorized(req, serviceToken) {
2
+ if (!serviceToken)
3
+ return true;
4
+ const authHeader = req.headers.authorization;
5
+ if (authHeader === `Bearer ${serviceToken}`)
6
+ return true;
7
+ const headerToken = req.headers["x-provider-core-token"];
8
+ return headerToken === serviceToken;
9
+ }
@@ -0,0 +1,2 @@
1
+ export declare function resolveProviderCoreWorkerCount(env?: NodeJS.ProcessEnv, cpuCount?: number): number;
2
+ export declare function startProviderCoreCluster(): void;
@@ -0,0 +1,44 @@
1
+ import cluster from "node:cluster";
2
+ import os from "node:os";
3
+ import { loadConfig } from "./config.js";
4
+ import { createProviderCoreServer } from "./server.js";
5
+ export function resolveProviderCoreWorkerCount(env = process.env, cpuCount = os.availableParallelism()) {
6
+ const requested = Number.parseInt(env.PROVIDER_CORE_WORKERS ?? "", 10);
7
+ const defaultWorkers = Math.min(2, Math.max(1, cpuCount));
8
+ const workers = Number.isFinite(requested) && requested > 0 ? requested : defaultWorkers;
9
+ return Math.max(1, Math.min(workers, Math.max(1, cpuCount)));
10
+ }
11
+ export function startProviderCoreCluster() {
12
+ if (!cluster.isPrimary) {
13
+ const config = loadConfig();
14
+ const server = createProviderCoreServer(config);
15
+ server.listen(config.port, config.host, () => {
16
+ console.log(`Provider Core worker ${process.pid} listening on http://${config.host}:${config.port}`);
17
+ });
18
+ return;
19
+ }
20
+ const workerCount = resolveProviderCoreWorkerCount();
21
+ console.log(`Provider Core cluster primary ${process.pid} starting ${workerCount} workers`);
22
+ let shuttingDown = false;
23
+ for (let i = 0; i < workerCount; i += 1) {
24
+ cluster.fork();
25
+ }
26
+ cluster.on("exit", (worker, code, signal) => {
27
+ if (shuttingDown)
28
+ return;
29
+ console.error(`Provider Core worker ${worker.process.pid ?? "unknown"} exited (${code ?? signal}); restarting`);
30
+ cluster.fork();
31
+ });
32
+ const shutdown = () => {
33
+ shuttingDown = true;
34
+ for (const worker of Object.values(cluster.workers ?? {})) {
35
+ worker?.disconnect();
36
+ }
37
+ setTimeout(() => process.exit(0), 20_000).unref();
38
+ };
39
+ process.on("SIGTERM", shutdown);
40
+ process.on("SIGINT", shutdown);
41
+ }
42
+ if (import.meta.url === `file://${process.argv[1]}`) {
43
+ startProviderCoreCluster();
44
+ }
@@ -0,0 +1,12 @@
1
+ export interface ProviderCoreServerConfig {
2
+ host: string;
3
+ port: number;
4
+ serviceToken?: string;
5
+ version: string;
6
+ runtime: "fixture" | "real";
7
+ credentialResolver: "env" | "external" | "external_optional";
8
+ keyPoolUrl?: string;
9
+ keyPoolToken?: string;
10
+ keyPoolTimeoutMs: number;
11
+ }
12
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): ProviderCoreServerConfig;
package/dist/config.js ADDED
@@ -0,0 +1,17 @@
1
+ export function loadConfig(env = process.env) {
2
+ return {
3
+ host: env.PROVIDER_CORE_HOST ?? "127.0.0.1",
4
+ port: Number(env.PROVIDER_CORE_PORT ?? "8787"),
5
+ serviceToken: env.PROVIDER_CORE_TOKEN,
6
+ version: env.PROVIDER_CORE_VERSION ?? "0.1.0",
7
+ runtime: env.PROVIDER_CORE_RUNTIME === "real" ? "real" : "fixture",
8
+ credentialResolver: env.PROVIDER_CORE_CREDENTIAL_RESOLVER === "external"
9
+ ? "external"
10
+ : env.PROVIDER_CORE_CREDENTIAL_RESOLVER === "external_optional"
11
+ ? "external_optional"
12
+ : "env",
13
+ keyPoolUrl: env.PROVIDER_CORE_KEY_POOL_URL,
14
+ keyPoolToken: env.PROVIDER_CORE_KEY_POOL_TOKEN,
15
+ keyPoolTimeoutMs: Number(env.PROVIDER_CORE_KEY_POOL_TIMEOUT_MS ?? "3000"),
16
+ };
17
+ }
@@ -0,0 +1,49 @@
1
+ import type { ProviderRegistry } from "@xiaozhiclaw/provider-core";
2
+ import type { ProviderCoreRequest } from "./request-normalizer.js";
3
+ export interface ResolvedCredential {
4
+ apiKey: string;
5
+ baseUrl?: string;
6
+ source: "env" | "key-pool";
7
+ leaseId?: string;
8
+ credentialId?: string;
9
+ egressMode?: "direct" | "regional_proxy";
10
+ egressRegion?: string;
11
+ egressEndpoint?: string;
12
+ }
13
+ export interface CredentialResolveContext {
14
+ request: ProviderCoreRequest;
15
+ registry: Pick<ProviderRegistry, "resolveApiKey">;
16
+ }
17
+ export interface CredentialReport {
18
+ success: boolean;
19
+ statusCode?: number;
20
+ errorType?: string;
21
+ errorMessage?: string;
22
+ latencyMs?: number;
23
+ promptTokens?: number;
24
+ completionTokens?: number;
25
+ }
26
+ export interface CredentialResolver {
27
+ resolve(context: CredentialResolveContext): Promise<ResolvedCredential>;
28
+ report?(credential: ResolvedCredential, report: CredentialReport): Promise<void>;
29
+ }
30
+ export declare class EnvCredentialResolver implements CredentialResolver {
31
+ resolve(context: CredentialResolveContext): Promise<ResolvedCredential>;
32
+ }
33
+ export interface ExternalKeyPoolResolverConfig {
34
+ url?: string;
35
+ token?: string;
36
+ mode: "external" | "external_optional";
37
+ timeoutMs?: number;
38
+ fallback?: CredentialResolver;
39
+ }
40
+ export declare class ExternalKeyPoolResolver implements CredentialResolver {
41
+ private readonly baseUrl;
42
+ private readonly token?;
43
+ private readonly mode;
44
+ private readonly timeoutMs;
45
+ private readonly fallback;
46
+ constructor(config: ExternalKeyPoolResolverConfig);
47
+ resolve(context: CredentialResolveContext): Promise<ResolvedCredential>;
48
+ report(credential: ResolvedCredential, report: CredentialReport): Promise<void>;
49
+ }
@@ -0,0 +1,116 @@
1
+ export class EnvCredentialResolver {
2
+ async resolve(context) {
3
+ const apiKey = context.registry.resolveApiKey(context.request.provider);
4
+ if (!apiKey)
5
+ throw new Error(`Missing API key for provider: ${context.request.provider}`);
6
+ return { apiKey, source: "env" };
7
+ }
8
+ }
9
+ export class ExternalKeyPoolResolver {
10
+ baseUrl;
11
+ token;
12
+ mode;
13
+ timeoutMs;
14
+ fallback;
15
+ constructor(config) {
16
+ if (!config.url && config.mode === "external") {
17
+ throw new Error("PROVIDER_CORE_KEY_POOL_URL is required when credential resolver is external");
18
+ }
19
+ this.baseUrl = (config.url ?? "").replace(/\/$/, "");
20
+ this.token = config.token;
21
+ this.mode = config.mode;
22
+ this.timeoutMs = config.timeoutMs ?? 3000;
23
+ this.fallback = config.fallback ?? new EnvCredentialResolver();
24
+ }
25
+ async resolve(context) {
26
+ if (!this.baseUrl)
27
+ return this.fallback.resolve(context);
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
30
+ try {
31
+ const metadata = context.request.metadata;
32
+ const response = await fetch(`${this.baseUrl}/lease`, {
33
+ method: "POST",
34
+ headers: {
35
+ "content-type": "application/json",
36
+ ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
37
+ },
38
+ body: JSON.stringify({
39
+ provider: context.request.provider,
40
+ public_model: context.request.metadata?.publicModel ?? context.request.model,
41
+ native_model: context.request.model,
42
+ request_id: context.request.metadata?.requestId ?? `provider_core_${Date.now().toString(36)}`,
43
+ protocol: context.request.protocol,
44
+ user_id: metadata?.userId,
45
+ session_id: metadata?.sessionId ?? metadata?.workspaceId,
46
+ }),
47
+ signal: controller.signal,
48
+ });
49
+ if (!response.ok)
50
+ throw new Error(`Key Pool lease failed: ${response.status}`);
51
+ const body = await response.json();
52
+ const data = body.data ?? {};
53
+ const apiKey = String(data.api_key ?? data.apiKey ?? "");
54
+ if (!apiKey)
55
+ throw new Error("Key Pool lease response missing api_key");
56
+ const egressMode = normalizeEgressMode(data.egress_mode ?? data.egressMode);
57
+ const egressEndpoint = typeof data.egress_endpoint === "string" ? data.egress_endpoint
58
+ : typeof data.egressEndpoint === "string" ? data.egressEndpoint
59
+ : undefined;
60
+ return {
61
+ apiKey,
62
+ baseUrl: resolveBaseUrlForEgress({
63
+ apiBase: typeof data.api_base === "string" ? data.api_base : typeof data.apiBase === "string" ? data.apiBase : undefined,
64
+ egressMode,
65
+ egressEndpoint,
66
+ }),
67
+ source: "key-pool",
68
+ leaseId: String(data.lease_id ?? data.leaseId ?? ""),
69
+ credentialId: String(data.credential_id ?? data.credentialId ?? ""),
70
+ egressMode,
71
+ egressRegion: String(data.egress_region ?? data.egressRegion ?? "local"),
72
+ egressEndpoint,
73
+ };
74
+ }
75
+ catch (error) {
76
+ if (this.mode === "external_optional") {
77
+ return this.fallback.resolve(context);
78
+ }
79
+ throw error;
80
+ }
81
+ finally {
82
+ clearTimeout(timer);
83
+ }
84
+ }
85
+ async report(credential, report) {
86
+ if (!credential.leaseId || !this.baseUrl)
87
+ return;
88
+ await fetch(`${this.baseUrl}/leases/${encodeURIComponent(credential.leaseId)}/report`, {
89
+ method: "POST",
90
+ headers: {
91
+ "content-type": "application/json",
92
+ ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
93
+ },
94
+ body: JSON.stringify({
95
+ success: report.success,
96
+ status_code: report.statusCode,
97
+ error_type: report.errorType,
98
+ error_message: report.errorMessage,
99
+ latency_ms: report.latencyMs,
100
+ prompt_tokens: report.promptTokens ?? 0,
101
+ completion_tokens: report.completionTokens ?? 0,
102
+ }),
103
+ });
104
+ }
105
+ }
106
+ function normalizeEgressMode(value) {
107
+ return value === "regional_proxy" ? "regional_proxy" : "direct";
108
+ }
109
+ function resolveBaseUrlForEgress({ apiBase, egressMode, egressEndpoint, }) {
110
+ if (egressMode === "direct")
111
+ return apiBase;
112
+ if (!egressEndpoint) {
113
+ throw new Error("Key Pool lease selected regional_proxy egress without egress_endpoint");
114
+ }
115
+ return egressEndpoint;
116
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProviderCoreServerConfig } from "./config.js";
2
+ export declare function healthPayload(config: ProviderCoreServerConfig): Record<string, unknown>;
package/dist/health.js ADDED
@@ -0,0 +1,8 @@
1
+ export function healthPayload(config) {
2
+ return {
3
+ status: "ok",
4
+ service: "provider-core-server",
5
+ version: config.version,
6
+ time: new Date().toISOString(),
7
+ };
8
+ }
@@ -0,0 +1,9 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export interface RequestContext {
3
+ requestId: string;
4
+ startMs: number;
5
+ }
6
+ export declare function createRequestContext(req: IncomingMessage): RequestContext;
7
+ export declare function attachRequestId(res: ServerResponse, context: RequestContext): void;
8
+ export declare function logRequest(req: IncomingMessage, context: RequestContext, message: string, extra?: Record<string, unknown>): void;
9
+ export declare function logRequestComplete(req: IncomingMessage, context: RequestContext, statusCode: number): void;
@@ -0,0 +1,28 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function createRequestContext(req) {
3
+ const headerRequestId = req.headers["x-request-id"];
4
+ return {
5
+ requestId: typeof headerRequestId === "string" && headerRequestId ? headerRequestId : randomUUID(),
6
+ startMs: Date.now(),
7
+ };
8
+ }
9
+ export function attachRequestId(res, context) {
10
+ res.setHeader("x-request-id", context.requestId);
11
+ }
12
+ export function logRequest(req, context, message, extra) {
13
+ console.log(JSON.stringify({
14
+ level: "info",
15
+ service: "provider-core-server",
16
+ requestId: context.requestId,
17
+ method: req.method,
18
+ url: req.url,
19
+ message,
20
+ ...extra,
21
+ }));
22
+ }
23
+ export function logRequestComplete(req, context, statusCode) {
24
+ logRequest(req, context, "request.complete", {
25
+ statusCode,
26
+ elapsedMs: Date.now() - context.startMs,
27
+ });
28
+ }
@@ -0,0 +1,16 @@
1
+ import type { LLMRequest } from "@xiaozhiclaw/provider-core";
2
+ export type PublicProtocol = "openai-chat" | "openai-responses" | "anthropic-messages";
3
+ export interface ProviderCoreRequest extends LLMRequest {
4
+ protocol: PublicProtocol;
5
+ provider: string;
6
+ metadata?: {
7
+ userId?: string;
8
+ workspaceId?: string;
9
+ sessionId?: string;
10
+ requestId?: string;
11
+ publicModel?: string;
12
+ source?: "qlogicagent" | "llmrouter";
13
+ };
14
+ providerOptions?: Record<string, unknown>;
15
+ }
16
+ export declare function normalizeProviderCoreRequest(body: unknown): ProviderCoreRequest;
@@ -0,0 +1,16 @@
1
+ export function normalizeProviderCoreRequest(body) {
2
+ if (!body || typeof body !== "object") {
3
+ throw new Error("Request body must be an object.");
4
+ }
5
+ const candidate = body;
6
+ if (!candidate.protocol || !["openai-chat", "openai-responses", "anthropic-messages"].includes(candidate.protocol)) {
7
+ throw new Error("protocol must be openai-chat, openai-responses, or anthropic-messages.");
8
+ }
9
+ if (!candidate.provider)
10
+ throw new Error("provider is required.");
11
+ if (!candidate.model)
12
+ throw new Error("model is required.");
13
+ if (!Array.isArray(candidate.messages))
14
+ throw new Error("messages must be an array.");
15
+ return candidate;
16
+ }
@@ -0,0 +1,24 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type ModelInfo, type ProviderDef, type ProviderEvent } from "@xiaozhiclaw/provider-core";
3
+ import type { ProviderCoreServerConfig } from "./config.js";
4
+ import { type ProviderCoreRequest } from "./request-normalizer.js";
5
+ export interface ProviderCoreRuntime {
6
+ listProviders(): ProviderDef[];
7
+ listModels(): Promise<ProviderCoreCatalogModel[]> | ProviderCoreCatalogModel[];
8
+ invoke(request: ProviderCoreRequest, signal?: AbortSignal): Promise<ProviderEvent[]>;
9
+ stream(request: ProviderCoreRequest, signal?: AbortSignal): AsyncIterable<ProviderEvent>;
10
+ }
11
+ export interface ProviderCoreCatalogModel extends ModelInfo {
12
+ provider: string;
13
+ providerName: string;
14
+ transport: ProviderDef["transport"];
15
+ nativeModelId: string;
16
+ }
17
+ export declare class FixtureProviderCoreRuntime implements ProviderCoreRuntime {
18
+ listProviders(): ProviderDef[];
19
+ listModels(): ProviderCoreCatalogModel[];
20
+ invoke(request: ProviderCoreRequest): Promise<ProviderEvent[]>;
21
+ stream(request: ProviderCoreRequest): AsyncIterable<ProviderEvent>;
22
+ }
23
+ export declare function routeProviderCoreRequest(req: IncomingMessage, res: ServerResponse, config: ProviderCoreServerConfig, runtime: ProviderCoreRuntime): Promise<void>;
24
+ export declare function flattenProviderModels(providers: ProviderDef[]): ProviderCoreCatalogModel[];
package/dist/routes.js ADDED
@@ -0,0 +1,106 @@
1
+ import { BUILTIN_PROVIDERS } from "@xiaozhiclaw/provider-core";
2
+ import { isAuthorized } from "./auth.js";
3
+ import { healthPayload } from "./health.js";
4
+ import { attachRequestId, createRequestContext, logRequest, logRequestComplete } from "./logging.js";
5
+ import { normalizeProviderCoreRequest } from "./request-normalizer.js";
6
+ import { startSse, writeProviderEvent } from "./sse.js";
7
+ export class FixtureProviderCoreRuntime {
8
+ listProviders() {
9
+ return BUILTIN_PROVIDERS;
10
+ }
11
+ listModels() {
12
+ return flattenProviderModels(BUILTIN_PROVIDERS);
13
+ }
14
+ async invoke(request) {
15
+ const events = [];
16
+ for await (const event of this.stream(request)) {
17
+ events.push(event);
18
+ }
19
+ return events;
20
+ }
21
+ async *stream(request) {
22
+ yield { type: "response.created", id: `resp_${Date.now()}`, model: request.model, provider: request.provider };
23
+ yield { type: "content.delta", index: 0, text: "Provider Core fixture response" };
24
+ yield { type: "usage.delta", usage: { promptTokens: request.messages.length, completionTokens: 4 } };
25
+ yield { type: "response.completed", usage: { promptTokens: request.messages.length, completionTokens: 4 }, finishReason: "stop" };
26
+ }
27
+ }
28
+ export async function routeProviderCoreRequest(req, res, config, runtime) {
29
+ const context = createRequestContext(req);
30
+ attachRequestId(res, context);
31
+ logRequest(req, context, "request.start");
32
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
33
+ if (url.pathname === "/health" && req.method === "GET") {
34
+ sendJson(res, 200, healthPayload(config));
35
+ logRequestComplete(req, context, 200);
36
+ return;
37
+ }
38
+ if (!isAuthorized(req, config.serviceToken)) {
39
+ sendJson(res, 401, { error: { message: "Unauthorized" } });
40
+ logRequestComplete(req, context, 401);
41
+ return;
42
+ }
43
+ if (url.pathname === "/v1/providers" && req.method === "GET") {
44
+ sendJson(res, 200, { providers: runtime.listProviders() });
45
+ logRequestComplete(req, context, 200);
46
+ return;
47
+ }
48
+ if (url.pathname === "/v1/models" && req.method === "GET") {
49
+ sendJson(res, 200, { models: await runtime.listModels() });
50
+ logRequestComplete(req, context, 200);
51
+ return;
52
+ }
53
+ if (url.pathname === "/v1/invoke" && req.method === "POST") {
54
+ const request = normalizeProviderCoreRequest(await readJson(req));
55
+ sendJson(res, 200, { events: await runtime.invoke(request) });
56
+ logRequestComplete(req, context, 200);
57
+ return;
58
+ }
59
+ if (url.pathname === "/v1/stream" && req.method === "POST") {
60
+ const controller = new AbortController();
61
+ req.on("close", () => controller.abort());
62
+ const request = normalizeProviderCoreRequest(await readJson(req));
63
+ startSse(res);
64
+ for await (const event of runtime.stream(request, controller.signal)) {
65
+ writeProviderEvent(res, event);
66
+ }
67
+ res.end();
68
+ logRequestComplete(req, context, 200);
69
+ return;
70
+ }
71
+ sendJson(res, 404, { error: { message: "Not found" } });
72
+ logRequestComplete(req, context, 404);
73
+ }
74
+ async function readJson(req) {
75
+ const chunks = [];
76
+ for await (const chunk of req) {
77
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
78
+ }
79
+ if (chunks.length === 0)
80
+ return {};
81
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
82
+ }
83
+ function sendJson(res, status, body) {
84
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
85
+ res.end(JSON.stringify(body));
86
+ }
87
+ export function flattenProviderModels(providers) {
88
+ const models = [];
89
+ const seen = new Set();
90
+ for (const provider of providers) {
91
+ for (const model of provider.models ?? []) {
92
+ const preferredPublicId = model.aliases?.[0] ?? model.id;
93
+ const publicId = seen.has(preferredPublicId) ? `${provider.id}/${preferredPublicId}` : preferredPublicId;
94
+ seen.add(publicId);
95
+ models.push({
96
+ ...model,
97
+ id: publicId,
98
+ provider: provider.id,
99
+ providerName: provider.name,
100
+ transport: provider.transport,
101
+ nativeModelId: model.id,
102
+ });
103
+ }
104
+ }
105
+ return models;
106
+ }
@@ -0,0 +1,15 @@
1
+ import { type ProviderDef, type ProviderEvent } from "@xiaozhiclaw/provider-core";
2
+ import type { ProviderCoreServerConfig } from "./config.js";
3
+ import { type CredentialResolver } from "./credential-resolver.js";
4
+ import type { ProviderCoreRequest } from "./request-normalizer.js";
5
+ import { type ProviderCoreCatalogModel, type ProviderCoreRuntime } from "./routes.js";
6
+ export declare class RealProviderCoreRuntime implements ProviderCoreRuntime {
7
+ private registry;
8
+ private credentialResolver;
9
+ constructor(config?: ProviderCoreServerConfig, credentialResolver?: CredentialResolver);
10
+ listProviders(): ProviderDef[];
11
+ listModels(): Promise<ProviderCoreCatalogModel[]>;
12
+ invoke(request: ProviderCoreRequest, signal?: AbortSignal): Promise<ProviderEvent[]>;
13
+ stream(request: ProviderCoreRequest, signal?: AbortSignal): AsyncIterable<ProviderEvent>;
14
+ private reportCredential;
15
+ }
@@ -0,0 +1,172 @@
1
+ import { createLLMClient, ProviderRegistry, } from "@xiaozhiclaw/provider-core";
2
+ import { EnvCredentialResolver, ExternalKeyPoolResolver } from "./credential-resolver.js";
3
+ import { flattenProviderModels } from "./routes.js";
4
+ export class RealProviderCoreRuntime {
5
+ registry = new ProviderRegistry();
6
+ credentialResolver;
7
+ constructor(config, credentialResolver) {
8
+ this.credentialResolver = credentialResolver ?? createCredentialResolver(config);
9
+ }
10
+ listProviders() {
11
+ return this.registry.listProviders();
12
+ }
13
+ async listModels() {
14
+ return flattenProviderModels(this.registry.listProviders().map((provider) => ({
15
+ ...provider,
16
+ models: this.registry.listModels(provider.id),
17
+ })));
18
+ }
19
+ async invoke(request, signal) {
20
+ const events = [];
21
+ for await (const event of this.stream(request, signal)) {
22
+ events.push(event);
23
+ }
24
+ return events;
25
+ }
26
+ async *stream(request, signal) {
27
+ const providerDef = this.registry.getProvider(request.provider);
28
+ if (!providerDef)
29
+ throw new Error(`Unknown provider: ${request.provider}`);
30
+ const started = Date.now();
31
+ let credential;
32
+ let success = true;
33
+ let lastUsage;
34
+ try {
35
+ credential = await this.credentialResolver.resolve({ request, registry: this.registry });
36
+ const client = createLLMClient({
37
+ provider: request.provider,
38
+ model: request.model,
39
+ apiKey: credential.apiKey,
40
+ baseUrl: credential.baseUrl,
41
+ }, this.registry);
42
+ const responseId = `resp_${Date.now().toString(36)}`;
43
+ yield {
44
+ type: "response.created",
45
+ id: responseId,
46
+ model: request.model,
47
+ provider: request.provider,
48
+ credentialId: credential.credentialId,
49
+ credentialSource: credential.source,
50
+ credentialEgressMode: credential.egressMode,
51
+ credentialEgressRegion: credential.egressRegion,
52
+ };
53
+ for await (const chunk of client.transport.stream(toLLMRequest(request), credential.apiKey, signal)) {
54
+ const event = chunkToProviderEvent(chunk, lastUsage);
55
+ if (!event)
56
+ continue;
57
+ if (chunk.type === "usage") {
58
+ lastUsage = {
59
+ promptTokens: chunk.promptTokens,
60
+ completionTokens: chunk.completionTokens,
61
+ reasoningTokens: chunk.reasoningTokens,
62
+ cacheReadTokens: chunk.cacheReadTokens,
63
+ cacheCreationTokens: chunk.cacheCreationTokens,
64
+ };
65
+ }
66
+ if (event.type === "error")
67
+ success = false;
68
+ if (event.type === "response.completed" && lastUsage && !event.usage) {
69
+ yield { ...event, usage: lastUsage };
70
+ }
71
+ else {
72
+ yield event;
73
+ }
74
+ }
75
+ await this.reportCredential(credential, {
76
+ success,
77
+ latencyMs: Date.now() - started,
78
+ promptTokens: lastUsage?.promptTokens,
79
+ completionTokens: lastUsage?.completionTokens,
80
+ });
81
+ }
82
+ catch (error) {
83
+ if (credential) {
84
+ await this.reportCredential(credential, {
85
+ success: false,
86
+ statusCode: statusCodeFromError(error),
87
+ errorType: error instanceof Error ? error.name : "provider_error",
88
+ errorMessage: error instanceof Error ? error.message : String(error),
89
+ latencyMs: Date.now() - started,
90
+ promptTokens: lastUsage?.promptTokens,
91
+ completionTokens: lastUsage?.completionTokens,
92
+ });
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+ async reportCredential(credential, report) {
98
+ try {
99
+ await this.credentialResolver.report?.(credential, report);
100
+ }
101
+ catch {
102
+ // Credential reporting must never mask a successful model response.
103
+ }
104
+ }
105
+ }
106
+ function createCredentialResolver(config) {
107
+ if (!config || config.credentialResolver === "env")
108
+ return new EnvCredentialResolver();
109
+ return new ExternalKeyPoolResolver({
110
+ url: config.keyPoolUrl,
111
+ token: config.keyPoolToken,
112
+ mode: config.credentialResolver,
113
+ timeoutMs: config.keyPoolTimeoutMs,
114
+ });
115
+ }
116
+ function statusCodeFromError(error) {
117
+ if (typeof error === "object" && error !== null) {
118
+ const candidate = error;
119
+ if (typeof candidate.status === "number")
120
+ return candidate.status;
121
+ if (typeof candidate.statusCode === "number")
122
+ return candidate.statusCode;
123
+ }
124
+ return undefined;
125
+ }
126
+ function toLLMRequest(request) {
127
+ const providerOptions = request.providerOptions ?? {};
128
+ return {
129
+ ...request,
130
+ ...providerOptions,
131
+ model: request.model,
132
+ messages: request.messages,
133
+ tools: request.tools,
134
+ };
135
+ }
136
+ function chunkToProviderEvent(chunk, lastUsage) {
137
+ switch (chunk.type) {
138
+ case "delta":
139
+ return { type: "content.delta", index: 0, text: chunk.text };
140
+ case "tool_call_delta":
141
+ return {
142
+ type: "tool.call.delta",
143
+ id: chunk.id ?? `tool_${chunk.index}`,
144
+ name: chunk.name,
145
+ argumentsDelta: chunk.arguments,
146
+ };
147
+ case "reasoning_delta":
148
+ return { type: "reasoning.delta", text: chunk.text };
149
+ case "usage":
150
+ return {
151
+ type: "usage.delta",
152
+ usage: {
153
+ promptTokens: chunk.promptTokens,
154
+ completionTokens: chunk.completionTokens,
155
+ reasoningTokens: chunk.reasoningTokens,
156
+ cacheReadTokens: chunk.cacheReadTokens,
157
+ cacheCreationTokens: chunk.cacheCreationTokens,
158
+ },
159
+ };
160
+ case "annotations":
161
+ return { type: "annotation", annotation: { annotations: chunk.annotations } };
162
+ case "builtin_tool_status":
163
+ return { type: "annotation", annotation: { toolType: chunk.toolType, event: chunk.event, data: chunk.data } };
164
+ case "error":
165
+ return { type: "error", error: { message: chunk.message } };
166
+ case "done":
167
+ return { type: "response.completed", usage: lastUsage, finishReason: chunk.finishReason };
168
+ case "response_id":
169
+ case "reasoning_block_complete":
170
+ return undefined;
171
+ }
172
+ }
@@ -0,0 +1,4 @@
1
+ import { type Server } from "node:http";
2
+ import { type ProviderCoreServerConfig } from "./config.js";
3
+ import { type ProviderCoreRuntime } from "./routes.js";
4
+ export declare function createProviderCoreServer(config?: ProviderCoreServerConfig, runtime?: ProviderCoreRuntime): Server;
package/dist/server.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createServer } from "node:http";
2
+ import { loadConfig } from "./config.js";
3
+ import { FixtureProviderCoreRuntime, routeProviderCoreRequest } from "./routes.js";
4
+ import { RealProviderCoreRuntime } from "./runtime.js";
5
+ export function createProviderCoreServer(config = loadConfig(), runtime = config.runtime === "real" ? new RealProviderCoreRuntime(config) : new FixtureProviderCoreRuntime()) {
6
+ return createServer((req, res) => {
7
+ routeProviderCoreRequest(req, res, config, runtime).catch((error) => {
8
+ res.writeHead(500, { "content-type": "application/json; charset=utf-8" });
9
+ res.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
10
+ });
11
+ });
12
+ }
13
+ if (import.meta.url === `file://${process.argv[1]}`) {
14
+ const config = loadConfig();
15
+ const server = createProviderCoreServer(config);
16
+ server.listen(config.port, config.host, () => {
17
+ console.log(`Provider Core Server listening on http://${config.host}:${config.port}`);
18
+ });
19
+ }
package/dist/sse.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { ServerResponse } from "node:http";
2
+ import type { ProviderEvent } from "@xiaozhiclaw/provider-core";
3
+ export declare function startSse(res: ServerResponse): void;
4
+ export declare function writeProviderEvent(res: ServerResponse, event: ProviderEvent): void;
package/dist/sse.js ADDED
@@ -0,0 +1,12 @@
1
+ export function startSse(res) {
2
+ res.writeHead(200, {
3
+ "content-type": "text/event-stream; charset=utf-8",
4
+ "cache-control": "no-cache, no-transform",
5
+ connection: "keep-alive",
6
+ "x-accel-buffering": "no",
7
+ });
8
+ }
9
+ export function writeProviderEvent(res, event) {
10
+ res.write(`event: ${event.type}\n`);
11
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
12
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@xiaozhiclaw/provider-core-server",
3
+ "version": "0.1.0",
4
+ "description": "Internal HTTP/SSE server for QLogic Provider Core",
5
+ "type": "module",
6
+ "main": "./dist/server.js",
7
+ "types": "./dist/server.d.ts",
8
+ "files": [
9
+ "dist/"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "engines": {
15
+ "node": ">=22.0.0"
16
+ },
17
+ "dependencies": {
18
+ "@xiaozhiclaw/provider-core": "^0.1.0"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "test": "vitest run"
23
+ }
24
+ }