@plurnk/plurnk-providers 1.3.4 → 1.3.6
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/.env.defaults +41 -52
- package/README.md +44 -53
- package/SPEC.md +215 -354
- package/dist/AiSdkProvider.d.ts +78 -0
- package/dist/AiSdkProvider.d.ts.map +1 -0
- package/dist/AiSdkProvider.js +591 -0
- package/dist/AiSdkProvider.js.map +1 -0
- package/dist/Mock.d.ts +1 -1
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +1 -1
- package/dist/Mock.js.map +1 -1
- package/dist/OpenAICompat.d.ts +3 -5
- package/dist/OpenAICompat.d.ts.map +1 -1
- package/dist/OpenAICompat.js +44 -133
- package/dist/OpenAICompat.js.map +1 -1
- package/dist/Pool.d.ts +1 -1
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +1 -1
- package/dist/Pool.js.map +1 -1
- package/dist/ProviderRegistry.d.ts.map +1 -1
- package/dist/ProviderRegistry.js +37 -24
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +52 -0
- package/dist/aiSdkTransport.d.ts.map +1 -0
- package/dist/aiSdkTransport.js +294 -0
- package/dist/aiSdkTransport.js.map +1 -0
- package/dist/catalogProvider.d.ts +15 -0
- package/dist/catalogProvider.d.ts.map +1 -0
- package/dist/catalogProvider.js +103 -0
- package/dist/catalogProvider.js.map +1 -0
- package/dist/compatibleProvider.d.ts +3 -0
- package/dist/compatibleProvider.d.ts.map +1 -0
- package/dist/compatibleProvider.js +146 -0
- package/dist/compatibleProvider.js.map +1 -0
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js.map +1 -1
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +13 -6
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/ollama.d.ts +3 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +39 -0
- package/dist/ollama.js.map +1 -0
- package/dist/openai.d.ts +2 -4
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +1 -2
- package/dist/openai.js.map +1 -1
- package/dist/sdkModels.d.ts +13 -0
- package/dist/sdkModels.d.ts.map +1 -0
- package/dist/sdkModels.js +153 -0
- package/dist/sdkModels.js.map +1 -0
- package/dist/standardProviders.d.ts +0 -1
- package/dist/standardProviders.d.ts.map +1 -1
- package/dist/standardProviders.js +9 -11
- package/dist/standardProviders.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +20 -9
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +4 -2
- package/dist/usage.js.map +1 -1
- package/package.json +19 -8
- package/src/{OpenAICompat.test.ts → AiSdkProvider.test.ts} +202 -177
- package/src/{OpenAICompat.ts → AiSdkProvider.ts} +89 -144
- package/src/Mock.test.ts +3 -3
- package/src/Mock.ts +1 -1
- package/src/Pool.test.ts +3 -3
- package/src/Pool.ts +1 -1
- package/src/ProviderRegistry.test.ts +40 -27
- package/src/ProviderRegistry.ts +35 -24
- package/src/aiSdkTransport.test.ts +253 -0
- package/src/aiSdkTransport.ts +369 -0
- package/src/boundaries.test.ts +2 -2
- package/src/catalogProvider.test.ts +100 -0
- package/src/catalogProvider.ts +151 -0
- package/src/compatibleProvider.test.ts +44 -0
- package/src/compatibleProvider.ts +205 -0
- package/src/discover.test.ts +12 -12
- package/src/discover.ts +3 -6
- package/src/env.ts +14 -6
- package/src/index.ts +7 -11
- package/src/ollama.ts +63 -0
- package/src/openai.ts +2 -8
- package/src/sdkModels.test.ts +47 -0
- package/src/sdkModels.ts +194 -0
- package/src/telemetry.test.ts +17 -10
- package/src/telemetry.ts +22 -14
- package/src/types.ts +10 -13
- package/src/usage.test.ts +8 -10
- package/src/usage.ts +7 -3
- package/src/openaiStream.ts +0 -310
- package/src/standardProviders.test.ts +0 -949
- package/src/standardProviders.ts +0 -635
package/src/sdkModels.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
|
|
2
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
+
import { createDeepInfra } from "@ai-sdk/deepinfra";
|
|
4
|
+
import { createGoogle } from "@ai-sdk/google";
|
|
5
|
+
import { createGroq } from "@ai-sdk/groq";
|
|
6
|
+
import { createMistral } from "@ai-sdk/mistral";
|
|
7
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
8
|
+
import { createTogetherAI } from "@ai-sdk/togetherai";
|
|
9
|
+
import { createXai } from "@ai-sdk/xai";
|
|
10
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
11
|
+
import { lookupProvider, type ProviderInfo } from "@plurnk/plurnk-models";
|
|
12
|
+
import type { LanguageModel } from "ai";
|
|
13
|
+
|
|
14
|
+
export type SdkModel = {
|
|
15
|
+
readonly languageModel?: LanguageModel;
|
|
16
|
+
readonly compatible?: {
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
19
|
+
};
|
|
20
|
+
readonly catalog: ProviderInfo | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const envPrefix = (provider: string): string =>
|
|
24
|
+
provider.replaceAll(/[^a-zA-Z0-9]/g, "_").toUpperCase();
|
|
25
|
+
|
|
26
|
+
export const configuredProviderInfo = (
|
|
27
|
+
provider: string,
|
|
28
|
+
env: NodeJS.ProcessEnv,
|
|
29
|
+
): ProviderInfo | null => {
|
|
30
|
+
const prefix = `PLURNK_PROVIDERS_PROVIDER_${envPrefix(provider)}`;
|
|
31
|
+
const npm = env[`${prefix}_NPM`];
|
|
32
|
+
if (npm === undefined || npm.length === 0) return null;
|
|
33
|
+
const keyNames = env[`${prefix}_API_KEY_ENV`]
|
|
34
|
+
?.split(",")
|
|
35
|
+
.map((name) => name.trim())
|
|
36
|
+
.filter(Boolean) ?? [];
|
|
37
|
+
const api = env[`${prefix}_BASE_URL`];
|
|
38
|
+
return {
|
|
39
|
+
id: provider,
|
|
40
|
+
npm,
|
|
41
|
+
env: keyNames,
|
|
42
|
+
...(api === undefined || api.length === 0 ? {} : { api }),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const firstSet = (env: NodeJS.ProcessEnv, names: readonly string[]): string | undefined => {
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
const value = env[name];
|
|
49
|
+
if (value !== undefined && value.length > 0) return value;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const configuredKeyNames = (
|
|
55
|
+
provider: string,
|
|
56
|
+
env: NodeJS.ProcessEnv,
|
|
57
|
+
catalog: ProviderInfo,
|
|
58
|
+
): readonly string[] => {
|
|
59
|
+
const configured = env[`PLURNK_PROVIDERS_PROVIDER_${envPrefix(provider)}_API_KEY_ENV`];
|
|
60
|
+
return configured === undefined || configured.length === 0
|
|
61
|
+
? catalog.env
|
|
62
|
+
: configured.split(",").map((name) => name.trim()).filter(Boolean);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const expandEnv = (value: string, env: NodeJS.ProcessEnv, provider: string): string =>
|
|
66
|
+
value.replaceAll(/\$\{([A-Z0-9_]+)\}/g, (_match, name: string) => {
|
|
67
|
+
const replacement = env[name];
|
|
68
|
+
if (replacement === undefined || replacement.length === 0) {
|
|
69
|
+
throw new Error(`${provider} provider: ${name} must be set to resolve the Models.dev API URL`);
|
|
70
|
+
}
|
|
71
|
+
return replacement;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const baseUrl = (
|
|
75
|
+
provider: string,
|
|
76
|
+
env: NodeJS.ProcessEnv,
|
|
77
|
+
catalog: ProviderInfo,
|
|
78
|
+
override?: string,
|
|
79
|
+
): string | undefined => {
|
|
80
|
+
const prefix = envPrefix(provider);
|
|
81
|
+
const configured = env[`PLURNK_PROVIDERS_PROVIDER_${prefix}_BASE_URL`]
|
|
82
|
+
?? env[`${prefix}_BASE_URL`]
|
|
83
|
+
?? catalog.api;
|
|
84
|
+
const value = override ?? configured;
|
|
85
|
+
return value === undefined ? undefined : expandEnv(value, env, provider).replace(/\/+$/, "");
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const apiKey = (
|
|
89
|
+
provider: string,
|
|
90
|
+
env: NodeJS.ProcessEnv,
|
|
91
|
+
catalog: ProviderInfo,
|
|
92
|
+
): string | undefined => firstSet(env, configuredKeyNames(provider, env, catalog));
|
|
93
|
+
|
|
94
|
+
const requireApiKey = (
|
|
95
|
+
provider: string,
|
|
96
|
+
env: NodeJS.ProcessEnv,
|
|
97
|
+
catalog: ProviderInfo,
|
|
98
|
+
): string => {
|
|
99
|
+
const names = configuredKeyNames(provider, env, catalog);
|
|
100
|
+
const key = firstSet(env, names);
|
|
101
|
+
if (key === undefined) throw new Error(`${provider} provider: ${names.join(" or ")} must be set`);
|
|
102
|
+
return key;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const createSdkModel = (
|
|
106
|
+
provider: string,
|
|
107
|
+
model: string,
|
|
108
|
+
env: NodeJS.ProcessEnv,
|
|
109
|
+
baseUrlOverride?: string,
|
|
110
|
+
): SdkModel | null => {
|
|
111
|
+
const catalog = lookupProvider(provider) ?? configuredProviderInfo(provider, env);
|
|
112
|
+
if (catalog === null) return null;
|
|
113
|
+
const url = baseUrl(provider, env, catalog, baseUrlOverride);
|
|
114
|
+
|
|
115
|
+
switch (catalog.npm) {
|
|
116
|
+
case "@ai-sdk/openai":
|
|
117
|
+
return {
|
|
118
|
+
languageModel: createOpenAI({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).chat(model),
|
|
119
|
+
catalog,
|
|
120
|
+
};
|
|
121
|
+
case "@ai-sdk/groq":
|
|
122
|
+
return {
|
|
123
|
+
languageModel: createGroq({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
124
|
+
catalog,
|
|
125
|
+
};
|
|
126
|
+
case "@ai-sdk/mistral":
|
|
127
|
+
return {
|
|
128
|
+
languageModel: createMistral({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
129
|
+
catalog,
|
|
130
|
+
};
|
|
131
|
+
case "@ai-sdk/togetherai":
|
|
132
|
+
return {
|
|
133
|
+
languageModel: createTogetherAI({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
134
|
+
catalog,
|
|
135
|
+
};
|
|
136
|
+
case "@ai-sdk/deepinfra":
|
|
137
|
+
return {
|
|
138
|
+
languageModel: createDeepInfra({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
139
|
+
catalog,
|
|
140
|
+
};
|
|
141
|
+
case "@ai-sdk/google":
|
|
142
|
+
return {
|
|
143
|
+
languageModel: createGoogle({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
144
|
+
catalog,
|
|
145
|
+
};
|
|
146
|
+
case "@ai-sdk/xai":
|
|
147
|
+
return {
|
|
148
|
+
languageModel: createXai({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
149
|
+
catalog,
|
|
150
|
+
};
|
|
151
|
+
case "@ai-sdk/anthropic":
|
|
152
|
+
return {
|
|
153
|
+
languageModel: createAnthropic({ apiKey: requireApiKey(provider, env, catalog), baseURL: url }).languageModel(model),
|
|
154
|
+
catalog,
|
|
155
|
+
};
|
|
156
|
+
case "@ai-sdk/amazon-bedrock":
|
|
157
|
+
return {
|
|
158
|
+
languageModel: createAmazonBedrock({
|
|
159
|
+
region: env.AWS_REGION ?? env.AWS_DEFAULT_REGION,
|
|
160
|
+
accessKeyId: env.AWS_ACCESS_KEY_ID,
|
|
161
|
+
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
|
|
162
|
+
sessionToken: env.AWS_SESSION_TOKEN,
|
|
163
|
+
apiKey: env.AWS_BEARER_TOKEN_BEDROCK,
|
|
164
|
+
baseURL: url,
|
|
165
|
+
}).languageModel(model),
|
|
166
|
+
catalog,
|
|
167
|
+
};
|
|
168
|
+
case "@openrouter/ai-sdk-provider":
|
|
169
|
+
return {
|
|
170
|
+
languageModel: createOpenRouter({
|
|
171
|
+
apiKey: requireApiKey(provider, env, catalog),
|
|
172
|
+
baseURL: url,
|
|
173
|
+
headers: {
|
|
174
|
+
...(env.OPENROUTER_HTTP_REFERER === undefined ? {} : { "HTTP-Referer": env.OPENROUTER_HTTP_REFERER }),
|
|
175
|
+
...(env.OPENROUTER_X_TITLE === undefined ? {} : { "X-Title": env.OPENROUTER_X_TITLE }),
|
|
176
|
+
},
|
|
177
|
+
}).languageModel(model),
|
|
178
|
+
catalog,
|
|
179
|
+
};
|
|
180
|
+
case "@ai-sdk/openai-compatible":
|
|
181
|
+
if (url === undefined) throw new Error(`${provider} provider: Models.dev supplies no API URL and no base URL was configured`);
|
|
182
|
+
return {
|
|
183
|
+
compatible: {
|
|
184
|
+
url: `${url}/chat/completions`,
|
|
185
|
+
headers: apiKey(provider, env, catalog) === undefined
|
|
186
|
+
? {}
|
|
187
|
+
: { Authorization: `Bearer ${apiKey(provider, env, catalog)}` },
|
|
188
|
+
},
|
|
189
|
+
catalog,
|
|
190
|
+
};
|
|
191
|
+
default:
|
|
192
|
+
throw new Error(`${provider} provider: Models.dev declares unsupported AI SDK package ${catalog.npm}`);
|
|
193
|
+
}
|
|
194
|
+
};
|
package/src/telemetry.test.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
+
import { APICallError } from "ai";
|
|
3
4
|
import { ProviderError, classifyProviderError, toProviderError, providerSource } from "./telemetry.ts";
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
const apiError = (statusCode: number, responseBody = "body") => new APICallError({
|
|
7
|
+
message: `request failed (${statusCode})`,
|
|
8
|
+
url: "https://example.test/v1/chat/completions",
|
|
9
|
+
requestBodyValues: {},
|
|
10
|
+
statusCode,
|
|
11
|
+
responseBody,
|
|
12
|
+
});
|
|
5
13
|
|
|
6
14
|
// Schema pattern for TelemetryEvent.source (from grammar's TelemetryEvent.json).
|
|
7
15
|
const SOURCE_PATTERN = /^[a-z]+(:[a-z][a-z0-9-]*)?$/;
|
|
@@ -13,7 +21,7 @@ test("providerSource produces a schema-valid colon-namespaced source", () => {
|
|
|
13
21
|
});
|
|
14
22
|
|
|
15
23
|
test("classifyProviderError maps HTTP status to kind", () => {
|
|
16
|
-
const k = (status: number) => classifyProviderError(
|
|
24
|
+
const k = (status: number) => classifyProviderError(apiError(status)).kind;
|
|
17
25
|
assert.equal(k(401), "unauthorized");
|
|
18
26
|
assert.equal(k(403), "unauthorized");
|
|
19
27
|
assert.equal(k(402), "quota_exceeded");
|
|
@@ -24,13 +32,11 @@ test("classifyProviderError maps HTTP status to kind", () => {
|
|
|
24
32
|
assert.equal(k(404), "invalid_response");
|
|
25
33
|
});
|
|
26
34
|
|
|
27
|
-
test("classifyProviderError: a 422 flagged grammar_invalid is
|
|
28
|
-
const rejected =
|
|
35
|
+
test("classifyProviderError: a 422 flagged grammar_invalid is distinct (#548); other 422s are invalid responses", () => {
|
|
36
|
+
const rejected = apiError(422, JSON.stringify({ error: { type: "grammar_invalid", message: "non-conforming emission rejected: ..." } }));
|
|
29
37
|
assert.equal(classifyProviderError(rejected).kind, "grammar_invalid");
|
|
30
|
-
|
|
31
|
-
assert.equal(classifyProviderError(
|
|
32
|
-
// a non-JSON 422 body (edge/proxy HTML) is terminal, and the sniff does not throw
|
|
33
|
-
assert.equal(classifyProviderError(new OpenAiHttpError(422, "<html>Bad</html>", null)).kind, "invalid_response");
|
|
38
|
+
assert.equal(classifyProviderError(apiError(422, JSON.stringify({ error: { type: "invalid_request_error" } }))).kind, "invalid_response");
|
|
39
|
+
assert.equal(classifyProviderError(apiError(422, "<html>Bad</html>")).kind, "invalid_response");
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
test("classifyProviderError treats non-HTTP errors as network_failure", () => {
|
|
@@ -49,11 +55,12 @@ test("ProviderError.toTelemetryEvent emits the canonical envelope", () => {
|
|
|
49
55
|
});
|
|
50
56
|
|
|
51
57
|
test("toProviderError classifies and tags an HTTP error with the source + status", () => {
|
|
52
|
-
const
|
|
58
|
+
const cause = apiError(401, "no key");
|
|
59
|
+
const pe = toProviderError(cause, "provider:groq");
|
|
53
60
|
assert.equal(pe.kind, "unauthorized");
|
|
54
61
|
assert.equal(pe.source, "provider:groq");
|
|
55
62
|
assert.equal(pe.status, 401);
|
|
56
|
-
assert.equal(pe.cause
|
|
63
|
+
assert.equal(pe.cause, cause);
|
|
57
64
|
});
|
|
58
65
|
|
|
59
66
|
test("toProviderError passes an existing ProviderError through unchanged", () => {
|
package/src/telemetry.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// dependency on grammar (see SPEC §11). Consumers route provider events through
|
|
7
7
|
// the same `source` + `kind` discriminator as parse/rail events.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { APICallError, RetryError } from "ai";
|
|
10
10
|
|
|
11
11
|
// Required by the schema: source (producer id) + kind (discriminator). message
|
|
12
12
|
// and position are optional. A transport failure isn't localizable, so it carries
|
|
@@ -29,11 +29,9 @@ export type ProviderTelemetryKind =
|
|
|
29
29
|
| "invalid_response"
|
|
30
30
|
| "unauthorized"
|
|
31
31
|
| "quota_exceeded"
|
|
32
|
-
// A 422 whose error.type is "grammar_invalid" (#548): the backend served
|
|
33
|
-
// generation and rejected it as non-conforming.
|
|
34
|
-
//
|
|
35
|
-
// grammar_unenforced below: that observes a SUCCEEDED exchange; this is a
|
|
36
|
-
// thrown transport failure the daemon retries, then fails as a leaf.
|
|
32
|
+
// A 422 whose error.type is "grammar_invalid" (#548): the backend served one
|
|
33
|
+
// generation and rejected it as non-conforming. This identifies the failure;
|
|
34
|
+
// whether to start another model exchange belongs to the engine.
|
|
37
35
|
| "grammar_invalid"
|
|
38
36
|
// Output did not conform to the GBNF. ALWAYS an observation, never a throw:
|
|
39
37
|
// a completed exchange returns its bytes with this event on
|
|
@@ -80,18 +78,27 @@ const wireErrorType = (body: string): string | null => {
|
|
|
80
78
|
// Map a thrown transport error to a (kind, message). Conservative; the message
|
|
81
79
|
// is factual, no guidance prose (consumer SPEC §15.1 policy).
|
|
82
80
|
export const classifyProviderError = (err: unknown): { kind: ProviderTelemetryKind; message: string } => {
|
|
83
|
-
if (err
|
|
84
|
-
|
|
81
|
+
if (RetryError.isInstance(err)) return classifyProviderError(err.lastError);
|
|
82
|
+
if (APICallError.isInstance(err)) {
|
|
83
|
+
const status = err.statusCode ?? 0;
|
|
84
|
+
const message = err.message;
|
|
85
|
+
const body = err.responseBody ?? "";
|
|
85
86
|
if (status === 401 || status === 403) return { kind: "unauthorized", message };
|
|
86
87
|
if (status === 402) return { kind: "quota_exceeded", message };
|
|
87
88
|
if (status === 429) return { kind: "rate_limit", message };
|
|
88
89
|
if (status >= 500) return { kind: "network_failure", message };
|
|
89
|
-
// A
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
if (status === 422 && wireErrorType(err.body) === "grammar_invalid") return { kind: "grammar_invalid", message };
|
|
90
|
+
// A flagged 422 identifies a grammar-rejected exchange. It is not
|
|
91
|
+
// transport replay policy; the engine decides whether to sample again.
|
|
92
|
+
if (status === 422 && wireErrorType(body) === "grammar_invalid") return { kind: "grammar_invalid", message };
|
|
93
93
|
return { kind: "invalid_response", message };
|
|
94
94
|
}
|
|
95
|
+
const wire = err as { message?: unknown; type?: unknown; status?: unknown };
|
|
96
|
+
if (wire?.type === "grammar_invalid") {
|
|
97
|
+
return {
|
|
98
|
+
kind: "grammar_invalid",
|
|
99
|
+
message: typeof wire.message === "string" ? wire.message : "grammar-invalid response",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
95
102
|
const e = err as { name?: string; message?: string };
|
|
96
103
|
const message = (e?.message ?? String(err)) || "request failed";
|
|
97
104
|
// Timeouts / fetch-level failures are transport, not a usable response.
|
|
@@ -102,7 +109,8 @@ export const classifyProviderError = (err: unknown): { kind: ProviderTelemetryKi
|
|
|
102
109
|
// Already-classified errors pass through unchanged.
|
|
103
110
|
export const toProviderError = (err: unknown, source: string): ProviderError => {
|
|
104
111
|
if (err instanceof ProviderError) return err;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
112
|
+
const underlying = RetryError.isInstance(err) ? err.lastError : err;
|
|
113
|
+
const { kind, message } = classifyProviderError(underlying);
|
|
114
|
+
const status = APICallError.isInstance(underlying) ? underlying.statusCode ?? null : null;
|
|
107
115
|
return new ProviderError(source, kind, message, { status, cause: err });
|
|
108
116
|
};
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// is the wire-reported CoT only.
|
|
4
4
|
|
|
5
5
|
import type { TelemetryEvent } from "./telemetry.ts";
|
|
6
|
+
import type { LanguageModel } from "ai";
|
|
6
7
|
|
|
7
8
|
export interface ChatMessage {
|
|
8
9
|
role: "system" | "user" | "assistant";
|
|
@@ -69,9 +70,9 @@ export interface ProviderResponse {
|
|
|
69
70
|
readonly assistant: ProviderAssistant;
|
|
70
71
|
readonly assistantRaw: unknown;
|
|
71
72
|
// Per-turn provider→client metadata bag: the backend's non-standard top-level
|
|
72
|
-
// response fields
|
|
73
|
-
//
|
|
74
|
-
//
|
|
73
|
+
// response fields passed through verbatim. Monetary values carry their own
|
|
74
|
+
// amount and currency; the provider does not reinterpret them. The consumer
|
|
75
|
+
// (service) merges this into its Turn metadata and
|
|
75
76
|
// filters what reaches the client; it reads `meta`, never mines `assistantRaw`.
|
|
76
77
|
// Absent when the backend reported no extra fields (#23, generalized).
|
|
77
78
|
readonly meta?: Record<string, unknown>;
|
|
@@ -191,9 +192,9 @@ export interface Provider {
|
|
|
191
192
|
// `tokenize === undefined` means the backend can't. Exact-counting
|
|
192
193
|
// consumers (the tokenizer seam) prefer this over any client-side data.
|
|
193
194
|
tokenize?(text: string): Promise<number[]>;
|
|
194
|
-
// Provider-owned cost calculation. Returns
|
|
195
|
+
// Provider-owned estimated cost calculation. Returns USD.
|
|
195
196
|
// Returns 0 for siblings/models with no known rates.
|
|
196
|
-
|
|
197
|
+
calculateCost(usage: ProviderUsage): number;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
// ProviderAlias moved to @plurnk/plurnk-aliases (the zero-dep parser, #27);
|
|
@@ -208,12 +209,8 @@ export interface ProviderOptions {
|
|
|
208
209
|
readonly baseUrl?: string;
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// optional third arg (per-alias overrides, e.g. baseUrl); a factory that ignores
|
|
216
|
-
// it keeps working unchanged.
|
|
217
|
-
export interface ProviderFactory {
|
|
218
|
-
fromEnv(env: NodeJS.ProcessEnv, model: string, options?: ProviderOptions): Provider | Promise<Provider>;
|
|
212
|
+
// A discovered provider plugin default-exports an AI SDK provider. PLURNK owns
|
|
213
|
+
// the adapter into Provider; the plugin owns only its protocol binding.
|
|
214
|
+
export interface AiSdkProviderPlugin {
|
|
215
|
+
languageModel(model: string): LanguageModel;
|
|
219
216
|
}
|
package/src/usage.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import { strict as assert } from "node:assert";
|
|
3
|
-
import { normalizeUsage,
|
|
3
|
+
import { normalizeUsage, calculateCostUsd } from "./usage.ts";
|
|
4
4
|
|
|
5
5
|
// — normalizeUsage —
|
|
6
6
|
|
|
@@ -115,22 +115,20 @@ test("normalizeUsage: no total reported -> re-split skipped, reasoning stays 0 (
|
|
|
115
115
|
assert.deepEqual(u, { prompt: 10, completion: 20, reasoning: 0, cached: 0, total: 30 });
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
// —
|
|
118
|
+
// — calculateCostUsd —
|
|
119
119
|
|
|
120
|
-
test("
|
|
120
|
+
test("calculateCostUsd: bills reasoning at the USD-per-million output rate", () => {
|
|
121
121
|
// 100 input, 0 cached, 50 completion + 200 reasoning = 250 output.
|
|
122
122
|
const usage = { prompt: 100, completion: 50, reasoning: 200, cached: 0, total: 350 };
|
|
123
|
-
|
|
124
|
-
assert.equal(computeCost(usage, { input: 1, output: 10, cached: 0 }), 2600);
|
|
123
|
+
assert.equal(calculateCostUsd(usage, { input: 1, output: 10, cached: 0 }), 0.0026);
|
|
125
124
|
});
|
|
126
125
|
|
|
127
|
-
test("
|
|
126
|
+
test("calculateCostUsd: cached prompt billed at the cache rate, remainder at input", () => {
|
|
128
127
|
const usage = { prompt: 1000, completion: 0, reasoning: 0, cached: 400, total: 1000 };
|
|
129
|
-
|
|
130
|
-
assert.equal(computeCost(usage, { input: 5, output: 99, cached: 1 }), 3400);
|
|
128
|
+
assert.equal(calculateCostUsd(usage, { input: 5, output: 99, cached: 1 }), 0.0034);
|
|
131
129
|
});
|
|
132
130
|
|
|
133
|
-
test("
|
|
131
|
+
test("calculateCostUsd: zero rates → 0", () => {
|
|
134
132
|
const usage = { prompt: 9, completion: 9, reasoning: 9, cached: 9, total: 27 };
|
|
135
|
-
assert.equal(
|
|
133
|
+
assert.equal(calculateCostUsd(usage, { input: 0, output: 0, cached: 0 }), 0);
|
|
136
134
|
});
|
package/src/usage.ts
CHANGED
|
@@ -69,14 +69,18 @@ export const normalizeUsage = (raw: RawUsage | null | undefined, reasoningText =
|
|
|
69
69
|
return { prompt, completion, reasoning, cached, total };
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Conventional provider pricing: USD per million tokens, matching Models.dev.
|
|
73
73
|
export type TokenRates = { input: number; output: number; cached: number };
|
|
74
74
|
|
|
75
75
|
// The one cost formula every provider uses: non-cached prompt at the input
|
|
76
76
|
// rate, cached prompt at the cache rate, and billable output (completion +
|
|
77
77
|
// reasoning) at the output rate.
|
|
78
|
-
export const
|
|
78
|
+
export const calculateCostUsd = (usage: ProviderUsage, rates: TokenRates): number => {
|
|
79
79
|
const nonCachedPrompt = Math.max(0, usage.prompt - usage.cached);
|
|
80
80
|
const output = usage.completion + usage.reasoning;
|
|
81
|
-
return
|
|
81
|
+
return (
|
|
82
|
+
nonCachedPrompt * rates.input
|
|
83
|
+
+ usage.cached * rates.cached
|
|
84
|
+
+ output * rates.output
|
|
85
|
+
) / 1_000_000;
|
|
82
86
|
};
|