@plurnk/plurnk-providers 1.18.0 → 1.19.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/.env.defaults +8 -4
- package/README.md +4 -16
- package/SPEC.md +41 -11
- package/dist/AiSdkProvider.d.ts +2 -4
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +7 -17
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/AiSdkRequestBody.d.ts +1 -4
- package/dist/AiSdkRequestBody.d.ts.map +1 -1
- package/dist/AiSdkRequestBody.js +10 -48
- package/dist/AiSdkRequestBody.js.map +1 -1
- package/dist/Mock.d.ts +2 -2
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +3 -3
- package/dist/Mock.js.map +1 -1
- package/dist/ProviderRegistry.js +2 -2
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/accounting.d.ts +0 -1
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +1 -7
- package/dist/accounting.js.map +1 -1
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +79 -7
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +13 -3
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +13 -25
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +1 -0
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +0 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/AiSdkProvider.test.ts +97 -126
- package/src/AiSdkProvider.ts +8 -22
- package/src/AiSdkRequestBody.ts +10 -42
- package/src/Mock.test.ts +11 -3
- package/src/Mock.ts +3 -3
- package/src/ProviderRegistry.test.ts +0 -1
- package/src/ProviderRegistry.ts +1 -1
- package/src/accounting.test.ts +0 -11
- package/src/accounting.ts +0 -8
- package/src/aiSdkTransport.test.ts +63 -1
- package/src/aiSdkTransport.ts +82 -7
- package/src/boundaries.test.ts +1 -1
- package/src/capacity.test.ts +2 -2
- package/src/catalogProvider.test.ts +46 -1
- package/src/catalogProvider.ts +12 -3
- package/src/compatibleProvider.test.ts +6 -6
- package/src/compatibleProvider.ts +13 -28
- package/src/cost.test.ts +3 -3
- package/src/discover.test.ts +11 -2
- package/src/env.test.ts +3 -0
- package/src/env.ts +2 -1
- package/src/errors.test.ts +2 -2
- package/src/index.ts +0 -1
- package/src/inputModalities.test.ts +2 -1
- package/src/sdkModels.test.ts +1 -1
- package/src/types.ts +8 -35
|
@@ -272,7 +272,7 @@ test("(#482) conformance judges the grant: tolerated overflow is accepted, past-
|
|
|
272
272
|
);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
test("request-observer open failures preserve the durability cause and issue no provider I/O", async () => {
|
|
275
|
+
test("{§provider-request-observer} request-observer open failures preserve the durability cause and issue no provider I/O", async () => {
|
|
276
276
|
const root = new Error("durable request open failed");
|
|
277
277
|
let calls = 0;
|
|
278
278
|
const provider = testProvider({
|
|
@@ -299,7 +299,7 @@ test("request-observer open failures preserve the durability cause and issue no
|
|
|
299
299
|
assert.equal(calls, 0);
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
test("request-observer settlement failures preserve the durability cause without retrying I/O", async () => {
|
|
302
|
+
test("{§provider-request-observer} request-observer settlement failures preserve the durability cause without retrying I/O", async () => {
|
|
303
303
|
const root = new Error("durable request settlement failed");
|
|
304
304
|
let calls = 0;
|
|
305
305
|
const provider = testProvider({
|
|
@@ -533,7 +533,7 @@ test("only the trailing eos_token is stripped; a quoted one mid-body survives",
|
|
|
533
533
|
assert.equal(res.assistant.content, "quotes <eos> in the body"); // only the tail goes
|
|
534
534
|
});
|
|
535
535
|
|
|
536
|
-
test("identity getters and default prompt estimate", async () => {
|
|
536
|
+
test("{§provider-prompt-measurement} identity getters and default prompt estimate", async () => {
|
|
537
537
|
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 1000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
|
|
538
538
|
assert.equal(p.model, "m");
|
|
539
539
|
assert.equal(p.contextWindow, null); // default
|
|
@@ -578,7 +578,7 @@ test("injected prompt measurement preserves provenance and request cost estimati
|
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
580
|
|
|
581
|
-
test("an exact request overflow rejects before observer or provider I/O", async () => {
|
|
581
|
+
test("{§provider-capacity-failure} an exact request overflow rejects before observer or provider I/O", async () => {
|
|
582
582
|
const calls = installFetch([{ choices: [{ delta: { content: "unreachable" } }] }]);
|
|
583
583
|
let observed = false;
|
|
584
584
|
const p = testProvider({
|
|
@@ -672,7 +672,7 @@ test("an unsupported fixed reasoning policy fails before provider I/O", () => {
|
|
|
672
672
|
);
|
|
673
673
|
});
|
|
674
674
|
|
|
675
|
-
test("native SDK warnings survive as source-attributed provider Notices", async () => {
|
|
675
|
+
test("{§provider-sdk-warning} native SDK warnings survive as source-attributed provider Notices", async () => {
|
|
676
676
|
const usage = {
|
|
677
677
|
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
678
678
|
outputTokens: { total: 1, text: 1, reasoning: 0 },
|
|
@@ -1251,7 +1251,7 @@ test("{§provider-tagged-reasoning} verbatim, non-leading, and structured-reason
|
|
|
1251
1251
|
});
|
|
1252
1252
|
|
|
1253
1253
|
test("{§provider-tagged-reasoning} grammar evidence retains the exact pre-projection tagged sentence", async () => {
|
|
1254
|
-
const content = "<think>🧠reason</think
|
|
1254
|
+
const content = "<think>🧠reason</think>````SEND\ndone\n````";
|
|
1255
1255
|
const config = {
|
|
1256
1256
|
...injectedBase,
|
|
1257
1257
|
contextWindow: 640,
|
|
@@ -1269,7 +1269,7 @@ test("{§provider-tagged-reasoning} grammar evidence retains the exact pre-proje
|
|
|
1269
1269
|
});
|
|
1270
1270
|
|
|
1271
1271
|
assert.equal(response.assistant.reasoning, "🧠reason");
|
|
1272
|
-
assert.equal(response.assistant.content, "
|
|
1272
|
+
assert.equal(response.assistant.content, "````SEND\ndone\n````");
|
|
1273
1273
|
assert.deepEqual(response.grammarEvidence, {
|
|
1274
1274
|
input: content,
|
|
1275
1275
|
contentStart: [..."<think>🧠reason</think>"].length,
|
|
@@ -1857,81 +1857,6 @@ test("meta: passes backend fields through without reinterpreting monetary values
|
|
|
1857
1857
|
assert.equal(res.meta?.system_fingerprint, "fp_abc");
|
|
1858
1858
|
});
|
|
1859
1859
|
|
|
1860
|
-
// — first-party telemetry headers ({§provider-request-authority}) —
|
|
1861
|
-
|
|
1862
|
-
const headerVal = (init: RequestInit, name: string): string | undefined =>
|
|
1863
|
-
new Headers(init.headers).get(name) ?? undefined;
|
|
1864
|
-
|
|
1865
|
-
test("firstPartyMetadata: attributions + client ride as Plurnk-* headers", async () => {
|
|
1866
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
1867
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1868
|
-
await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0", "@foo/y@0.3.1"], client: "plurnk.nvim/1.4.0" });
|
|
1869
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), '["@acme/x@1.2.0","@foo/y@0.3.1"]');
|
|
1870
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Client"), "plurnk.nvim/1.4.0");
|
|
1871
|
-
});
|
|
1872
|
-
|
|
1873
|
-
test("Plurnk-Call-Kind carries the caller's emission or bare output contract under the first-party gate", async () => {
|
|
1874
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
1875
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1876
|
-
await p.generate({ workerId: "emission", messages: [], callKind: "emission" });
|
|
1877
|
-
await p.generate({ workerId: "bare", messages: [], callKind: "bare" });
|
|
1878
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Call-Kind"), "emission");
|
|
1879
|
-
assert.equal(headerVal(calls[1].init, "Plurnk-Call-Kind"), "bare");
|
|
1880
|
-
});
|
|
1881
|
-
|
|
1882
|
-
test("generate rejects an unknown call kind before provider I/O", async () => {
|
|
1883
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
1884
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1885
|
-
await assert.rejects(
|
|
1886
|
-
p.generate({ workerId: "invalid", messages: [], callKind: "unknown" as never }),
|
|
1887
|
-
/unsupported callKind "unknown"/,
|
|
1888
|
-
);
|
|
1889
|
-
assert.equal(calls.length, 0);
|
|
1890
|
-
});
|
|
1891
|
-
|
|
1892
|
-
test("Plurnk-Worker-Primary: the lineage root rides under the gate; emitted even when it equals workerId", async () => {
|
|
1893
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
1894
|
-
let calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1895
|
-
await p.generate({ workerId: "w-child", primaryWorkerId: "w-root", messages: [] });
|
|
1896
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), "w-root"); // a descendant: Primary != Worker-Id
|
|
1897
|
-
mock.restoreAll();
|
|
1898
|
-
|
|
1899
|
-
// the primary worker's own turn: Primary == Worker-Id, still stamped (never skipped on equality)
|
|
1900
|
-
calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1901
|
-
await p.generate({ workerId: "w-root", primaryWorkerId: "w-root", messages: [] });
|
|
1902
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), "w-root");
|
|
1903
|
-
mock.restoreAll();
|
|
1904
|
-
|
|
1905
|
-
// absent when the consumer supplies none — the provider never invents a primary
|
|
1906
|
-
calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1907
|
-
await p.generate({ workerId: "w-root", messages: [] });
|
|
1908
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), undefined);
|
|
1909
|
-
});
|
|
1910
|
-
|
|
1911
|
-
test("Plurnk-Worker-Primary is structurally dropped when firstPartyMetadata is off", async () => {
|
|
1912
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
|
|
1913
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1914
|
-
await p.generate({ workerId: "w-child", primaryWorkerId: "w-root", messages: [] });
|
|
1915
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Worker-Primary"), undefined); // never reaches a third-party backend
|
|
1916
|
-
});
|
|
1917
|
-
|
|
1918
|
-
test("firstPartyMetadata off (default): the headers are structurally dropped even when values are passed", async () => {
|
|
1919
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
|
|
1920
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1921
|
-
await p.generate({ workerId: "r", messages: [], attributions: ["@acme/x@1.2.0"], client: "plurnk-cli/2.0.0", callKind: "bare" });
|
|
1922
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), undefined); // never leaks to a non-first-party backend
|
|
1923
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Client"), undefined);
|
|
1924
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Call-Kind"), undefined);
|
|
1925
|
-
});
|
|
1926
|
-
|
|
1927
|
-
test("firstPartyMetadata on but empty values: no header emitted", async () => {
|
|
1928
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
1929
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
1930
|
-
await p.generate({ workerId: "r", messages: [], attributions: [], client: "" });
|
|
1931
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Attribution"), undefined);
|
|
1932
|
-
assert.equal(headerVal(calls[0].init, "Plurnk-Client"), undefined);
|
|
1933
|
-
});
|
|
1934
|
-
|
|
1935
1860
|
test("grammar transport: no grammar passed sends no grammar field, but the penalty rides", async () => {
|
|
1936
1861
|
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, grammarStyle: "llamacpp" });
|
|
1937
1862
|
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
@@ -2048,7 +1973,7 @@ test("streaming:false: a non-ok response rejects as a classified ProviderError (
|
|
|
2048
1973
|
});
|
|
2049
1974
|
});
|
|
2050
1975
|
|
|
2051
|
-
test("generate fail-hards on a missing or empty workerId", async () => {
|
|
1976
|
+
test("{§provider-cache-identity} generate fail-hards on a missing or empty workerId", async () => {
|
|
2052
1977
|
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
|
|
2053
1978
|
installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
2054
1979
|
await assert.rejects(() => p.generate({ workerId: "", messages: [] }), /workerId is required/);
|
|
@@ -2096,6 +2021,58 @@ test("{§provider-input-modalities} compatible transports serialize image files
|
|
|
2096
2021
|
}]);
|
|
2097
2022
|
});
|
|
2098
2023
|
|
|
2024
|
+
for (const [mediaType, format] of [["audio/wav", "wav"], ["audio/mpeg", "mp3"]]) {
|
|
2025
|
+
test(`{§provider-input-modalities} ${mediaType} reaches the compatible wire as native input_audio`, async () => {
|
|
2026
|
+
const provider = testProvider({
|
|
2027
|
+
model: "audio-model", url: "https://example.test/v1/chat/completions", fetchTimeoutMs: 5000,
|
|
2028
|
+
temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
|
|
2029
|
+
});
|
|
2030
|
+
const calls = installFetch([{ choices: [{ delta: { content: "heard" } }] }]);
|
|
2031
|
+
const bytes = new Uint8Array([82, 73, 70, 70]);
|
|
2032
|
+
await provider.generate({ workerId: "audio", messages: [{ role: "user", content: [
|
|
2033
|
+
{ type: "text", text: "listen" }, { type: "file", data: bytes, mediaType: mediaType! },
|
|
2034
|
+
] }] });
|
|
2035
|
+
assert.deepEqual(JSON.parse(String(calls[0]?.init.body)).messages, [{ role: "user", content: [
|
|
2036
|
+
{ type: "text", text: "listen" },
|
|
2037
|
+
{ type: "input_audio", input_audio: { data: Buffer.from(bytes).toString("base64"), format } },
|
|
2038
|
+
] }]);
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
test("{§provider-input-modalities} Google serializes native audio through its own SDK adapter", async () => {
|
|
2043
|
+
const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
|
|
2044
|
+
const google = createGoogleGenerativeAI({ apiKey: "fixture", baseURL: "https://example.test/v1beta" });
|
|
2045
|
+
const calls = installFetchJson({ candidates: [{ content: { role: "model", parts: [{ text: "heard" }] }, finishReason: "STOP" }] });
|
|
2046
|
+
const provider = testProvider({
|
|
2047
|
+
model: "audio-fixture", languageModel: google("audio-fixture"), fetchTimeoutMs: 5000,
|
|
2048
|
+
temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
|
|
2049
|
+
streaming: false,
|
|
2050
|
+
});
|
|
2051
|
+
const bytes = new Uint8Array([82, 73, 70, 70]);
|
|
2052
|
+
await provider.generate({ workerId: "audio", messages: [{ role: "user", content: [
|
|
2053
|
+
{ type: "text", text: "listen" }, { type: "file", data: bytes, mediaType: "audio/wav" },
|
|
2054
|
+
] }] });
|
|
2055
|
+
assert.deepEqual(JSON.parse(String(calls[0]?.init.body)).contents, [{ role: "user", parts: [
|
|
2056
|
+
{ text: "listen" }, { inlineData: { mimeType: "audio/wav", data: Buffer.from(bytes).toString("base64") } },
|
|
2057
|
+
] }]);
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
test("{§provider-input-modalities} an unsupported audio codec fails visibly instead of dropping its part", async () => {
|
|
2061
|
+
const provider = testProvider({
|
|
2062
|
+
model: "audio-model", url: "https://example.test/v1/chat/completions", fetchTimeoutMs: 5000,
|
|
2063
|
+
temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0,
|
|
2064
|
+
});
|
|
2065
|
+
const calls = installFetch([]);
|
|
2066
|
+
await assert.rejects(provider.generate({ workerId: "audio", messages: [{ role: "user", content: [
|
|
2067
|
+
{ type: "file", data: new Uint8Array([79, 103, 103, 83]), mediaType: "audio/ogg" },
|
|
2068
|
+
] }] }), (error: unknown) => {
|
|
2069
|
+
assert.ok(error instanceof ProviderError);
|
|
2070
|
+
assert.match(error.message, /audio\/ogg/u);
|
|
2071
|
+
return true;
|
|
2072
|
+
});
|
|
2073
|
+
assert.equal(calls.length, 0, "no text-only substitute is sent to the provider");
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2099
2076
|
test("generate wraps an HTTP failure as a ProviderError carrying Problem Details", async () => {
|
|
2100
2077
|
const { ProviderError } = await import("./errors.ts");
|
|
2101
2078
|
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, source: "provider:test" });
|
|
@@ -2572,40 +2549,6 @@ test("caller sampling cannot forge logprobs (reserved keys): the env flag is the
|
|
|
2572
2549
|
assert.equal("top_logprobs" in body, false);
|
|
2573
2550
|
mock.restoreAll();
|
|
2574
2551
|
});
|
|
2575
|
-
|
|
2576
|
-
// — turn coordinate headers ({§lifecycle-terms}): same gate as every first-party signal —
|
|
2577
|
-
|
|
2578
|
-
test("workspaceId/loop/turn ride as Plurnk-Workspace-Id/Loop/Turn under the first-party gate", async () => {
|
|
2579
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
2580
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
2581
|
-
await p.generate({ workerId: "r", messages: [], workspaceId: "s-9", loop: 3, turn: 41 });
|
|
2582
|
-
const headers = new Headers(calls[0].init.headers);
|
|
2583
|
-
assert.equal(headers.get("plurnk-workspace-id"), "s-9");
|
|
2584
|
-
assert.equal(headers.get("plurnk-loop"), "3");
|
|
2585
|
-
assert.equal(headers.get("plurnk-turn"), "41");
|
|
2586
|
-
});
|
|
2587
|
-
|
|
2588
|
-
test("third-party providers structurally DROP the coordinate (gate off by default)", async () => {
|
|
2589
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0 });
|
|
2590
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
2591
|
-
await p.generate({ workerId: "r", messages: [], workspaceId: "s-9", loop: 3, turn: 41 });
|
|
2592
|
-
const headers = new Headers(calls[0].init.headers);
|
|
2593
|
-
assert.equal(headers.has("plurnk-workspace-id"), false);
|
|
2594
|
-
assert.equal(headers.has("plurnk-loop"), false);
|
|
2595
|
-
assert.equal(headers.has("plurnk-turn"), false);
|
|
2596
|
-
});
|
|
2597
|
-
|
|
2598
|
-
test("coordinates are 1-based — 0/absent/empty emit no header", async () => {
|
|
2599
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, reasoning: { mode: "off", budget: null }, retryAttempts: 0, firstPartyMetadata: true });
|
|
2600
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
2601
|
-
await p.generate({ workerId: "r", messages: [], workspaceId: "", loop: 0, turn: 0 });
|
|
2602
|
-
const headers = new Headers(calls[0].init.headers);
|
|
2603
|
-
assert.equal(headers.has("plurnk-workspace-id"), false);
|
|
2604
|
-
assert.equal(headers.has("plurnk-loop"), false);
|
|
2605
|
-
assert.equal(headers.has("plurnk-turn"), false);
|
|
2606
|
-
assert.equal(headers.has("plurnk-strikes"), false);
|
|
2607
|
-
});
|
|
2608
|
-
|
|
2609
2552
|
// -- {§provider-generation-envelope} --
|
|
2610
2553
|
|
|
2611
2554
|
test("the adapter exposes independent model limits and the resolved generation envelope", () => {
|
|
@@ -2626,15 +2569,6 @@ test("the adapter exposes independent model limits and the resolved generation e
|
|
|
2626
2569
|
assert.equal(unknown.outputBudget, null);
|
|
2627
2570
|
});
|
|
2628
2571
|
|
|
2629
|
-
test("router-owned tuning: tuningFloors:false drops the temperature/penalty floors, caller sampling still rides", async () => {
|
|
2630
|
-
const p = testProvider({ model: "m", url: "http://x/v1/chat/completions", fetchTimeoutMs: 5000, temperature: 0.2, repeatPenalty: 1.15, frequencyPenalty: 0.4, reasoning: { mode: "off", budget: null }, retryAttempts: 0, tuningFloors: false });
|
|
2631
|
-
const calls = installFetch([{ choices: [{ delta: { content: "x" } }] }]);
|
|
2632
|
-
await p.generate({ workerId: "r", messages: [], sampling: { temperature: 0.9 } });
|
|
2633
|
-
const body = JSON.parse(calls[0].init.body as string);
|
|
2634
|
-
assert.equal(body.temperature, 0.9); // caller intent passes verbatim
|
|
2635
|
-
assert.equal("frequency_penalty" in body, false); // the floor is suppressed; the router owns tuning
|
|
2636
|
-
});
|
|
2637
|
-
|
|
2638
2572
|
// -- {§provider-cache-affinity} / {§provider-cache-write-policy} --
|
|
2639
2573
|
|
|
2640
2574
|
test("a compatible route's declared body affinity is managed by workerId", async () => {
|
|
@@ -2988,3 +2922,40 @@ test("a response whose usage counters contradict each other is delivered with un
|
|
|
2988
2922
|
const raw = response.assistantRaw as { usageRefusal?: { reason: string; usage: unknown } };
|
|
2989
2923
|
assert.deepEqual(raw.usageRefusal, { reason: "provider usage.outputTokenDetails.textTokens must be a non-negative safe integer", usage }, "the durable response carries the refused counters");
|
|
2990
2924
|
});
|
|
2925
|
+
|
|
2926
|
+
test("{§provider-connectivity} a stream still producing content outlives the attempt deadline; stream-idle governs it instead", async () => {
|
|
2927
|
+
const encoder = new TextEncoder();
|
|
2928
|
+
const chunk = (content: string, finish: string | null = null) => `data: ${JSON.stringify({
|
|
2929
|
+
id: "long", object: "chat.completion.chunk", created: 1, model: "m",
|
|
2930
|
+
choices: [{ index: 0, delta: { content }, finish_reason: finish }],
|
|
2931
|
+
})}\n\n`;
|
|
2932
|
+
mock.method(globalThis, "fetch", async () => new Response(new ReadableStream({
|
|
2933
|
+
async start(controller) {
|
|
2934
|
+
for (let index = 0; index < 8; index += 1) {
|
|
2935
|
+
controller.enqueue(encoder.encode(chunk(`part${index} `)));
|
|
2936
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2937
|
+
}
|
|
2938
|
+
controller.enqueue(encoder.encode(chunk("", "stop")));
|
|
2939
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
2940
|
+
controller.close();
|
|
2941
|
+
},
|
|
2942
|
+
}), { headers: { "content-type": "text/event-stream" } }));
|
|
2943
|
+
try {
|
|
2944
|
+
const p = testProvider({
|
|
2945
|
+
model: "m",
|
|
2946
|
+
url: "http://x/v1/chat/completions",
|
|
2947
|
+
fetchTimeoutMs: 60,
|
|
2948
|
+
streamIdleTimeoutMs: 1_000,
|
|
2949
|
+
temperature: 0.2,
|
|
2950
|
+
repeatPenalty: 1.15,
|
|
2951
|
+
reasoning: { mode: "off", budget: null },
|
|
2952
|
+
retryAttempts: 1,
|
|
2953
|
+
source: "provider:test",
|
|
2954
|
+
operationTimeoutMs: 5_000,
|
|
2955
|
+
});
|
|
2956
|
+
const response = await p.generate({ workerId: "long", messages: [] });
|
|
2957
|
+
assert.equal(response.assistant.content, "part0 part1 part2 part3 part4 part5 part6 part7 ", "about 200 ms of streaming against a 60 ms attempt deadline completes");
|
|
2958
|
+
} finally {
|
|
2959
|
+
mock.restoreAll();
|
|
2960
|
+
}
|
|
2961
|
+
});
|
package/src/AiSdkProvider.ts
CHANGED
|
@@ -54,7 +54,7 @@ const raceAgainstDeadline = async <T>(work: PromiseLike<T>, signal: AbortSignal)
|
|
|
54
54
|
|
|
55
55
|
// Backend wire spellings for the resolved reasoning intent. The switch beside each
|
|
56
56
|
// mapping retains any backend-specific omission/explicit-disable constraint.
|
|
57
|
-
export type ReasoningStyle = "none" | "think" | "include_reasoning" | "effort" | "effort_explicit" | "effort_required" | "thinking_effort" | "template" | "anthropic";
|
|
57
|
+
export type ReasoningStyle = "none" | "think" | "include_reasoning" | "effort" | "effort_explicit" | "effort_required" | "thinking_effort" | "thinking_config" | "template" | "anthropic";
|
|
58
58
|
|
|
59
59
|
export type NativeReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
60
60
|
export type CompatibleReasoningEffort = NativeReasoningEffort | "max";
|
|
@@ -126,7 +126,6 @@ export type AiSdkProviderConfig = {
|
|
|
126
126
|
// a fixed deployment choice and therefore wins on every request.
|
|
127
127
|
serviceTier?: string;
|
|
128
128
|
streaming?: boolean; // SSE transport (default true); false → one non-streamed JSON
|
|
129
|
-
firstPartyMetadata?: boolean; // forward per-turn attributions + client as Plurnk-* headers (plurnk only); default false
|
|
130
129
|
apiKeyRejectedMessage?: string; // friendly hint when a present key is 401/403-rejected (distinct from unset); default undefined
|
|
131
130
|
eosText?: string; // server-reported eos_token, stripped from the content tail (--special renders it as text); default undefined
|
|
132
131
|
// Slot affinity wiring is provider-internal, never consumer-facing.
|
|
@@ -199,10 +198,6 @@ export type AiSdkProviderConfig = {
|
|
|
199
198
|
// the standard factory always supplies them. Resolved against contextWindow
|
|
200
199
|
// at read time (getters), so a probe that lands after config assembly still
|
|
201
200
|
// derives correctly.
|
|
202
|
-
// The plurnk.ai router owns tuning — false suppresses the
|
|
203
|
-
// client-side temperature/penalty FLOORS on this provider (caller `sampling`
|
|
204
|
-
// still passes through verbatim). Default true (floors ride).
|
|
205
|
-
tuningFloors?: boolean;
|
|
206
201
|
};
|
|
207
202
|
|
|
208
203
|
class ProviderRequestObserverError extends Error {
|
|
@@ -296,13 +291,11 @@ export default class AiSdkProvider implements Provider {
|
|
|
296
291
|
#reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
|
|
297
292
|
#serviceTier: string | undefined;
|
|
298
293
|
#streaming: boolean;
|
|
299
|
-
#firstPartyMetadata: boolean;
|
|
300
294
|
#supportsSlotPinning: boolean;
|
|
301
295
|
#slotCount: number | null;
|
|
302
296
|
#retryAttempts: number;
|
|
303
297
|
#errorDetailLimit: number | undefined;
|
|
304
298
|
#topLogprobs: number | null;
|
|
305
|
-
#tuningFloors: boolean;
|
|
306
299
|
#rawBody: boolean;
|
|
307
300
|
#servedModel: string | undefined;
|
|
308
301
|
#requiresOutputBudget: boolean | undefined;
|
|
@@ -418,14 +411,12 @@ export default class AiSdkProvider implements Provider {
|
|
|
418
411
|
}
|
|
419
412
|
this.#serviceTier = config.serviceTier;
|
|
420
413
|
this.#streaming = config.streaming ?? true;
|
|
421
|
-
this.#firstPartyMetadata = config.firstPartyMetadata ?? false;
|
|
422
414
|
this.#apiKeyRejectedMessage = config.apiKeyRejectedMessage;
|
|
423
415
|
this.#eosText = config.eosText;
|
|
424
416
|
this.#hasApiKey = "Authorization" in this.#headers;
|
|
425
417
|
this.#supportsSlotPinning = config.supportsSlotPinning ?? false;
|
|
426
418
|
this.#slotCount = config.slotCount ?? null;
|
|
427
419
|
this.#topLogprobs = config.topLogprobs ?? null;
|
|
428
|
-
this.#tuningFloors = config.tuningFloors ?? true;
|
|
429
420
|
this.#rawBody = config.rawBody ?? false;
|
|
430
421
|
this.#servedModel = config.servedModel;
|
|
431
422
|
this.#requiresOutputBudget = config.requiresOutputBudget;
|
|
@@ -477,7 +468,7 @@ export default class AiSdkProvider implements Provider {
|
|
|
477
468
|
return tokens;
|
|
478
469
|
};
|
|
479
470
|
}
|
|
480
|
-
this.#requestBody = new AiSdkRequestBody({ reasoningBudget: this.#reasoningBudget, additiveReasoningProvider: this.#additiveReasoningProvider, reasoning: this.#reasoning, reasoningToggle: this.#reasoningToggle, compatibleAdaptiveReasoning: this.#compatibleAdaptiveReasoning, compatibleOffReasoning: this.#compatibleOffReasoning, adaptiveReasoningProviderOptions: this.#adaptiveReasoningProviderOptions, repeatPenalty: this.#repeatPenalty, frequencyPenalty: this.#frequencyPenalty, dryMultiplier: this.#dryMultiplier, dryBase: this.#dryBase, dryAllowedLength: this.#dryAllowedLength, repeatLastN: this.#repeatLastN, reasoningStyle: this.#reasoningStyle, source: this.#source, grammarStyle: this.#grammarStyle, cacheAffinity: this.#cacheAffinity, reasoningResponseProviderOptions: this.#reasoningResponseProviderOptions,
|
|
471
|
+
this.#requestBody = new AiSdkRequestBody({ reasoningBudget: this.#reasoningBudget, additiveReasoningProvider: this.#additiveReasoningProvider, reasoning: this.#reasoning, reasoningToggle: this.#reasoningToggle, compatibleAdaptiveReasoning: this.#compatibleAdaptiveReasoning, compatibleOffReasoning: this.#compatibleOffReasoning, adaptiveReasoningProviderOptions: this.#adaptiveReasoningProviderOptions, repeatPenalty: this.#repeatPenalty, frequencyPenalty: this.#frequencyPenalty, dryMultiplier: this.#dryMultiplier, dryBase: this.#dryBase, dryAllowedLength: this.#dryAllowedLength, repeatLastN: this.#repeatLastN, reasoningStyle: this.#reasoningStyle, source: this.#source, grammarStyle: this.#grammarStyle, cacheAffinity: this.#cacheAffinity, reasoningResponseProviderOptions: this.#reasoningResponseProviderOptions, supportsSlotPinning: this.#supportsSlotPinning, slotCount: this.#slotCount });
|
|
481
472
|
}
|
|
482
473
|
|
|
483
474
|
get contextWindow(): number | null { return this.#contextWindow; }
|
|
@@ -613,7 +604,7 @@ export default class AiSdkProvider implements Provider {
|
|
|
613
604
|
});
|
|
614
605
|
}
|
|
615
606
|
|
|
616
|
-
async generate({ messages, workerId,
|
|
607
|
+
async generate({ messages, workerId, signal, grammar, maxOutputTokens, sampling, observeRequest, observeReasoning, callKind }: ProviderGenerateArgs): Promise<ProviderResponse> {
|
|
617
608
|
// {§provider-interface} The worker identity is required.
|
|
618
609
|
if (workerId === undefined || workerId.length === 0) throw new Error("generate: workerId is required — the worker's stable, opaque identity");
|
|
619
610
|
if (callKind !== undefined && callKind !== "emission" && callKind !== "bare") {
|
|
@@ -655,9 +646,8 @@ export default class AiSdkProvider implements Provider {
|
|
|
655
646
|
// paths and the name promises every request) < the caller's `sampling`
|
|
656
647
|
// < the managed fields, which always win.
|
|
657
648
|
const body: Record<string, unknown> = {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
...(this.#tuningFloors ? { ...(this.#temperature !== null ? { temperature: this.#temperature } : {}), ...this.#requestBody.repetitionPenaltyBody() } : {}),
|
|
649
|
+
...(this.#temperature !== null ? { temperature: this.#temperature } : {}),
|
|
650
|
+
...this.#requestBody.repetitionPenaltyBody(),
|
|
661
651
|
...this.#requestBody.samplingBody(sampling),
|
|
662
652
|
...(this.#serviceTier !== undefined ? { service_tier: this.#serviceTier } : {}),
|
|
663
653
|
...this.#requestBody.reasoningBody(preserveGrammarSentence, capacity.reasoningBudget),
|
|
@@ -672,13 +662,11 @@ export default class AiSdkProvider implements Provider {
|
|
|
672
662
|
: {}),
|
|
673
663
|
};
|
|
674
664
|
|
|
675
|
-
// Per-request headers = static auth/routing
|
|
676
|
-
const metaHeaders = this.#requestBody.metadataHeaders(attributions, client, strikes, workerId, primaryWorkerId, workspaceId, loop, turn, callKind);
|
|
665
|
+
// Per-request headers = static auth/routing only.
|
|
677
666
|
const headers = new Headers(this.#headers);
|
|
678
667
|
if (this.#cacheAffinity?.target === "header") {
|
|
679
668
|
headers.set(this.#cacheAffinity.name, workerId);
|
|
680
669
|
}
|
|
681
|
-
for (const [name, value] of Object.entries(metaHeaders)) headers.set(name, value);
|
|
682
670
|
const requestHeaders = Object.fromEntries(headers.entries());
|
|
683
671
|
const accounting: ProviderRequestAccounting[] = [];
|
|
684
672
|
const operationTimeout = this.#operationTimeoutMs > 0
|
|
@@ -803,15 +791,13 @@ export default class AiSdkProvider implements Provider {
|
|
|
803
791
|
streaming: this.#streaming,
|
|
804
792
|
captureRawBody: this.#rawBody,
|
|
805
793
|
...observers,
|
|
806
|
-
temperature: this.#
|
|
807
|
-
? (typeof sampling?.temperature === "number" ? sampling.temperature : this.#temperature ?? undefined)
|
|
808
|
-
: typeof sampling?.temperature === "number" ? sampling.temperature : undefined,
|
|
794
|
+
temperature: typeof sampling?.temperature === "number" ? sampling.temperature : this.#temperature ?? undefined,
|
|
809
795
|
topP: typeof sampling?.top_p === "number" ? sampling.top_p : undefined,
|
|
810
796
|
topK: typeof sampling?.top_k === "number" ? sampling.top_k : undefined,
|
|
811
797
|
presencePenalty: typeof sampling?.presence_penalty === "number" ? sampling.presence_penalty : undefined,
|
|
812
798
|
frequencyPenalty: typeof sampling?.frequency_penalty === "number"
|
|
813
799
|
? sampling.frequency_penalty
|
|
814
|
-
: this.#
|
|
800
|
+
: this.#frequencyPenalty > 0 ? this.#frequencyPenalty : undefined,
|
|
815
801
|
stopSequences: typeof sampling?.stop === "string"
|
|
816
802
|
? [sampling.stop]
|
|
817
803
|
: Array.isArray(sampling?.stop) && sampling.stop.every((value) => typeof value === "string")
|
package/src/AiSdkRequestBody.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The provider-specific request body and headers one generate call sends: reasoning, grammar, sampling, repetition, slots, metadata. Split out of AiSdkProvider; every knob it reads is injected.
|
|
2
|
-
import type {
|
|
2
|
+
import type { ReasoningPolicy } from "./types.ts";
|
|
3
3
|
import type { JSONValue } from "ai";
|
|
4
4
|
import { type Reasoning } from "./env.ts";
|
|
5
5
|
import { fixedEffort } from "./reasoning-effort.ts";
|
|
@@ -80,13 +80,12 @@ export default class AiSdkRequestBody {
|
|
|
80
80
|
readonly #grammarStyle: GrammarStyle;
|
|
81
81
|
readonly #cacheAffinity: CacheAffinity | undefined;
|
|
82
82
|
readonly #reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
|
|
83
|
-
readonly #firstPartyMetadata: boolean;
|
|
84
83
|
readonly #supportsSlotPinning: boolean;
|
|
85
84
|
readonly #slotCount: number | null;
|
|
86
85
|
#runSlots = new Map<string, number>();
|
|
87
86
|
#nextSlot = 0;
|
|
88
87
|
|
|
89
|
-
constructor({ reasoningBudget, additiveReasoningProvider, reasoning, reasoningToggle, compatibleAdaptiveReasoning, compatibleOffReasoning, adaptiveReasoningProviderOptions, repeatPenalty, frequencyPenalty, dryMultiplier, dryBase, dryAllowedLength, repeatLastN, reasoningStyle, source, grammarStyle, cacheAffinity, reasoningResponseProviderOptions,
|
|
88
|
+
constructor({ reasoningBudget, additiveReasoningProvider, reasoning, reasoningToggle, compatibleAdaptiveReasoning, compatibleOffReasoning, adaptiveReasoningProviderOptions, repeatPenalty, frequencyPenalty, dryMultiplier, dryBase, dryAllowedLength, repeatLastN, reasoningStyle, source, grammarStyle, cacheAffinity, reasoningResponseProviderOptions, supportsSlotPinning, slotCount }: {
|
|
90
89
|
reasoningBudget: number | null;
|
|
91
90
|
additiveReasoningProvider: "anthropic" | "bedrock" | undefined;
|
|
92
91
|
reasoning: Reasoning;
|
|
@@ -105,7 +104,6 @@ export default class AiSdkRequestBody {
|
|
|
105
104
|
grammarStyle: GrammarStyle;
|
|
106
105
|
cacheAffinity: CacheAffinity | undefined;
|
|
107
106
|
reasoningResponseProviderOptions: AiSdkProviderOptions | undefined;
|
|
108
|
-
firstPartyMetadata: boolean;
|
|
109
107
|
supportsSlotPinning: boolean;
|
|
110
108
|
slotCount: number | null;
|
|
111
109
|
}) {
|
|
@@ -127,7 +125,6 @@ export default class AiSdkRequestBody {
|
|
|
127
125
|
this.#grammarStyle = grammarStyle;
|
|
128
126
|
this.#cacheAffinity = cacheAffinity;
|
|
129
127
|
this.#reasoningResponseProviderOptions = reasoningResponseProviderOptions;
|
|
130
|
-
this.#firstPartyMetadata = firstPartyMetadata;
|
|
131
128
|
this.#supportsSlotPinning = supportsSlotPinning;
|
|
132
129
|
this.#slotCount = slotCount;
|
|
133
130
|
}
|
|
@@ -204,6 +201,14 @@ export default class AiSdkRequestBody {
|
|
|
204
201
|
thinking: { type: "enabled" },
|
|
205
202
|
reasoning_effort: fixedEffort(mode),
|
|
206
203
|
};
|
|
204
|
+
// {§google-reasoning-request} — Gemini's OpenAI-compatible extension. Readable
|
|
205
|
+
// thoughts ride on every reasoning request; Gemini refuses `reasoning_effort` beside a
|
|
206
|
+
// thinking_config, so the level travels inside it. Gemini cannot turn reasoning off.
|
|
207
|
+
case "thinking_config": {
|
|
208
|
+
if (mode === "off") throw new TypeError(`${this.#source}: thinking_config reasoning has no off projection`);
|
|
209
|
+
const level = mode === "adaptive" ? {} : { thinking_level: fixedEffort(mode) };
|
|
210
|
+
return { extra_body: { google: { thinking_config: { include_thoughts: true, ...level } } } };
|
|
211
|
+
}
|
|
207
212
|
// Anthropic-compatible native dynamic or manual budget mode.
|
|
208
213
|
case "anthropic": return mode === "off"
|
|
209
214
|
? { thinking: { type: "disabled" } }
|
|
@@ -294,43 +299,6 @@ export default class AiSdkRequestBody {
|
|
|
294
299
|
}
|
|
295
300
|
|
|
296
301
|
|
|
297
|
-
// First-party telemetry headers ({§provider-request-authority} {§provider-call-kind}): forwarded only when the spec
|
|
298
|
-
// opted in (the plurnk endpoint). The gate is here, not at the call site, so
|
|
299
|
-
// attributions/client/strikes can never reach a third-party backend even if
|
|
300
|
-
// the consumer passes them to the wrong provider. Empty values emit no header
|
|
301
|
-
// — EXCEPT strikes, where 0 is a real value (clean streak) distinct from
|
|
302
|
-
// absent (consumer didn't report); contract {§strikes-first-party-metadata}. Strikes
|
|
303
|
-
// ride HTTP headers only — the packet never carries them (the model must
|
|
304
|
-
// never see strike state; engine accounting is not a metric to game).
|
|
305
|
-
metadataHeaders(attributions: string[] | undefined, client: string | undefined, strikes: number | undefined, workerId: string, primaryWorkerId: string | undefined, workspaceId: string | undefined, loop: number | undefined, turn: number | undefined, callKind: ProviderCallKind | undefined): Record<string, string> {
|
|
306
|
-
if (!this.#firstPartyMetadata) return {};
|
|
307
|
-
const h: Record<string, string> = {};
|
|
308
|
-
if (attributions !== undefined && attributions.length > 0) h["Plurnk-Attribution"] = JSON.stringify(attributions);
|
|
309
|
-
if (client !== undefined && client.length > 0) h["Plurnk-Client"] = client;
|
|
310
|
-
if (strikes !== undefined && Number.isInteger(strikes) && strikes >= 0) h["Plurnk-Strikes"] = String(strikes);
|
|
311
|
-
// Worker identity: the opaque workerId
|
|
312
|
-
// the consumer already supplies, forwarded so the endpoint can key
|
|
313
|
-
// per-worker affinity/telemetry — same gate as every first-party signal.
|
|
314
|
-
h["Plurnk-Worker-Id"] = workerId;
|
|
315
|
-
// Root worker of the lineage ({§worker-primary}): the no-parent ancestor of this turn's
|
|
316
|
-
// worker tree. The consumer classifies primary-vs-spawned by equality
|
|
317
|
-
// (primaryWorkerId == workerId ⇒ the primary/root worker). The provider
|
|
318
|
-
// EMITS what the consumer supplies and never invents a primary; the
|
|
319
|
-
// consumer's contract is to stamp it EVERY turn (including the primary's
|
|
320
|
-
// own, where it equals workerId). Absence is the consumer's violation for
|
|
321
|
-
// the endpoint to surface, not a provider default.
|
|
322
|
-
if (primaryWorkerId !== undefined && primaryWorkerId.length > 0) h["Plurnk-Worker-Primary"] = primaryWorkerId;
|
|
323
|
-
// Turn coordinate ({§lifecycle-terms}): workspace/loop/turn, the
|
|
324
|
-
// daemon-side sequence the endpoint can never scrape from the wire.
|
|
325
|
-
// Coordinates are 1-based — 0 is not a real value, so no strikes-style
|
|
326
|
-
// zero exception; absent/empty/0 emits no header.
|
|
327
|
-
if (workspaceId !== undefined && workspaceId.length > 0) h["Plurnk-Workspace-Id"] = workspaceId;
|
|
328
|
-
if (loop !== undefined && Number.isInteger(loop) && loop >= 1) h["Plurnk-Loop"] = String(loop);
|
|
329
|
-
if (turn !== undefined && Number.isInteger(turn) && turn >= 1) h["Plurnk-Turn"] = String(turn);
|
|
330
|
-
if (callKind !== undefined) h["Plurnk-Call-Kind"] = callKind;
|
|
331
|
-
return h;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
302
|
|
|
335
303
|
requestProviderOptions(
|
|
336
304
|
workerId: string,
|
package/src/Mock.test.ts
CHANGED
|
@@ -8,10 +8,18 @@ import { ProviderError } from "./errors.ts";
|
|
|
8
8
|
const build = (responses: MockResponse[] = [{ assistant: { content: "hi", reasoning: null } }]) =>
|
|
9
9
|
new Mock({ contextWindow: 100000, responses });
|
|
10
10
|
|
|
11
|
+
// The suite runs on this package's own panel, which ships an output budget. "No budget" is therefore
|
|
12
|
+
// something a test states — the panel's explicit empty value — never an ambient absence of the key.
|
|
13
|
+
const withoutOutputBudget = <T>(body: () => T): T => {
|
|
14
|
+
const panel = process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET;
|
|
15
|
+
process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET = "";
|
|
16
|
+
try { return body(); } finally { process.env.PLURNK_PROVIDERS_OUTPUT_BUDGET = panel; }
|
|
17
|
+
};
|
|
18
|
+
|
|
11
19
|
// — Identity ({§provider-interface}) —
|
|
12
20
|
|
|
13
21
|
test("Mock: contextWindow and model are stable across reads", () => {
|
|
14
|
-
const m = build();
|
|
22
|
+
const m = withoutOutputBudget(() => build());
|
|
15
23
|
assert.equal(m.contextWindow, 100000);
|
|
16
24
|
assert.equal(m.inputCapacity, null);
|
|
17
25
|
assert.equal(m.contextWindow, 100000);
|
|
@@ -193,8 +201,8 @@ test("Mock resolves percentage and absolute generation budgets against its windo
|
|
|
193
201
|
}
|
|
194
202
|
});
|
|
195
203
|
|
|
196
|
-
test("
|
|
197
|
-
const m = new Mock({ contextWindow: 49152, responses: [] });
|
|
204
|
+
test("an empty generation budget leaves a bare Mock unbounded", () => {
|
|
205
|
+
const m = withoutOutputBudget(() => new Mock({ contextWindow: 49152, responses: [] }));
|
|
198
206
|
assert.equal(m.outputBudget, null);
|
|
199
207
|
assert.equal(m.reasoningBudget, null);
|
|
200
208
|
assert.equal(m.inputCapacity, null);
|
package/src/Mock.ts
CHANGED
|
@@ -41,7 +41,7 @@ export type MockResponse = {
|
|
|
41
41
|
export type MockReturnedAssistant = ProviderAssistant & { ops?: unknown[] };
|
|
42
42
|
export type MockReturnedResponse = ProviderResponse & { assistant: MockReturnedAssistant };
|
|
43
43
|
|
|
44
|
-
const
|
|
44
|
+
const MOCK_USAGE: ProviderUsage = {
|
|
45
45
|
inputTokens: 0,
|
|
46
46
|
outputTokens: 0,
|
|
47
47
|
totalTokens: 0,
|
|
@@ -154,7 +154,7 @@ export default class Mock implements Provider {
|
|
|
154
154
|
}
|
|
155
155
|
const a = next.assistant;
|
|
156
156
|
const usage: ProviderUsage = {
|
|
157
|
-
...
|
|
157
|
+
...MOCK_USAGE,
|
|
158
158
|
...next.usage,
|
|
159
159
|
};
|
|
160
160
|
const requestAccounting: ProviderRequestAccounting = validateProviderRequestAccounting({
|
|
@@ -196,4 +196,4 @@ export default class Mock implements Provider {
|
|
|
196
196
|
get remaining(): number { return this.#queue.length; }
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
export {
|
|
199
|
+
export { MOCK_USAGE as mockDefaultUsage };
|
package/src/ProviderRegistry.ts
CHANGED
|
@@ -60,7 +60,7 @@ export const instantiateProvider = async (
|
|
|
60
60
|
const catalog = catalogProviderFromEnv(name, env, model, baseUrl);
|
|
61
61
|
if (catalog !== null) return catalog;
|
|
62
62
|
if (name === "ollama") return ollamaProviderFromEnv(env, model, baseUrl === undefined ? undefined : { baseUrl });
|
|
63
|
-
if (name === "openai"
|
|
63
|
+
if (name === "openai") return compatibleProviderFromEnv(env, model, baseUrl);
|
|
64
64
|
const { registry, skipped, packageAttributions = new Map(), grammarStyles = new Map() } = await providerPackages(discoverFn, env);
|
|
65
65
|
const specifier = registry.get(name);
|
|
66
66
|
if (specifier === undefined) {
|
package/src/accounting.test.ts
CHANGED
|
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
aggregateProviderAccounting,
|
|
5
|
-
plurnkCostNormalizer,
|
|
6
5
|
providerCostNormalizer,
|
|
7
6
|
} from "./accounting.ts";
|
|
8
7
|
|
|
@@ -48,16 +47,6 @@ test("DeepInfra's documented response estimate remains estimated", () => {
|
|
|
48
47
|
});
|
|
49
48
|
});
|
|
50
49
|
|
|
51
|
-
test("first-party charged evidence is validated at its adapter boundary", () => {
|
|
52
|
-
const charged = {
|
|
53
|
-
kind: "charged",
|
|
54
|
-
amount: { amount: "0.01", currency: "USD" },
|
|
55
|
-
source: "plurnk endpoint",
|
|
56
|
-
} as const;
|
|
57
|
-
assert.deepEqual(plurnkCostNormalizer(evidence({ charge: charged })), charged);
|
|
58
|
-
assert.equal(plurnkCostNormalizer(evidence({})), undefined);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
50
|
test("response cost normalization is an explicit adapter capability", () => {
|
|
62
51
|
assert.equal(providerCostNormalizer("@ai-sdk/anthropic"), undefined);
|
|
63
52
|
assert.equal(providerCostNormalizer("@ai-sdk/xai")!(evidence({ usage: {} })), undefined);
|