@frockbot/plugin-provider-ollama-cloud 0.0.0 → 0.1.1
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/README.md +115 -1
- package/frockbot.json +79 -0
- package/package.json +39 -6
- package/src/client.test.ts +140 -0
- package/src/client.ts +357 -0
- package/src/endpoint.test.ts +412 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/runtime.test.ts +518 -0
- package/src/runtime.ts +193 -0
- package/src/user.test.ts +2230 -0
- package/src/user.ts +2444 -0
- package/src/web-search.test.ts +383 -0
- package/src/web-search.ts +299 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
LlmEffectNotStartedError,
|
|
4
|
+
type NormalizedModelRequest,
|
|
5
|
+
} from "@frockbot/kernel-contracts";
|
|
6
|
+
import { type Agent } from "@frockbot/kernel-agent-loop/agent";
|
|
7
|
+
import { LlmRegistry } from "@frockbot/plugin-models";
|
|
8
|
+
import {
|
|
9
|
+
openCredentialV1,
|
|
10
|
+
parseCredentialKeyringV1,
|
|
11
|
+
sealCredentialV1,
|
|
12
|
+
type CredentialLeaseV1,
|
|
13
|
+
} from "@frockbot/connection-core";
|
|
14
|
+
import { OpenAICompatibleHttpError } from "@frockbot/provider-openai-compatible";
|
|
15
|
+
import { Context, Service } from "cordis";
|
|
16
|
+
import {
|
|
17
|
+
createOllamaCloudRuntimePlugin,
|
|
18
|
+
ollamaChatBaseUrl,
|
|
19
|
+
} from "./runtime.js";
|
|
20
|
+
|
|
21
|
+
function serializedKeyring(): string {
|
|
22
|
+
const bytes = Uint8Array.from({ length: 32 }, (_, index) => index + 11);
|
|
23
|
+
let binary = "";
|
|
24
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
25
|
+
const key = btoa(binary)
|
|
26
|
+
.replaceAll("+", "-")
|
|
27
|
+
.replaceAll("/", "_")
|
|
28
|
+
.replace(/=+$/, "");
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
currentKeyId: "primary",
|
|
32
|
+
keys: { primary: key },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class TestCredentialLeaseRuntime extends Service {
|
|
37
|
+
private readonly keyring;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
ctx: Context,
|
|
41
|
+
serializedKeyring: string,
|
|
42
|
+
private readonly onOpen: () => void = () => undefined,
|
|
43
|
+
) {
|
|
44
|
+
super(ctx, "credentialLease");
|
|
45
|
+
this.keyring = parseCredentialKeyringV1(serializedKeyring);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
open(input: {
|
|
49
|
+
accountId: string;
|
|
50
|
+
connectionId: string;
|
|
51
|
+
packageId: string;
|
|
52
|
+
lease: CredentialLeaseV1;
|
|
53
|
+
}): Promise<string> {
|
|
54
|
+
this.onOpen();
|
|
55
|
+
return openCredentialV1({
|
|
56
|
+
keyring: this.keyring,
|
|
57
|
+
context: {
|
|
58
|
+
accountId: input.accountId,
|
|
59
|
+
connectionId: input.connectionId,
|
|
60
|
+
packageId: input.packageId,
|
|
61
|
+
credentialGeneration: input.lease.credentialGeneration,
|
|
62
|
+
},
|
|
63
|
+
envelope: input.lease.envelope,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function mountCredentialRuntime(
|
|
69
|
+
root: Context,
|
|
70
|
+
keyring = serializedKeyring(),
|
|
71
|
+
onOpen?: () => void,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
await root.plugin((ctx) => {
|
|
74
|
+
new TestCredentialLeaseRuntime(ctx, keyring, onOpen);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const request: NormalizedModelRequest = {
|
|
79
|
+
requestId: "effect-1",
|
|
80
|
+
provider: "ollama-cloud",
|
|
81
|
+
model: "glm-5.3-flash:cloud",
|
|
82
|
+
system: "",
|
|
83
|
+
messages: [{ role: "user", content: "hello" }],
|
|
84
|
+
tools: [],
|
|
85
|
+
modelBinding: {
|
|
86
|
+
connectionId: "connection-1",
|
|
87
|
+
connectionGeneration: "generation-1",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe("Ollama Cloud runtime Contribution", () => {
|
|
92
|
+
test("resolves one credential generation per effect inside the provider", async () => {
|
|
93
|
+
const keyringText = serializedKeyring();
|
|
94
|
+
const envelope = await sealCredentialV1({
|
|
95
|
+
keyring: parseCredentialKeyringV1(keyringText),
|
|
96
|
+
context: {
|
|
97
|
+
accountId: "account-1",
|
|
98
|
+
connectionId: "connection-1",
|
|
99
|
+
packageId: "provider-ollama-cloud",
|
|
100
|
+
credentialGeneration: "generation-1",
|
|
101
|
+
},
|
|
102
|
+
plaintext: "account-secret",
|
|
103
|
+
});
|
|
104
|
+
const authorizations: string[] = [];
|
|
105
|
+
const leasedGenerations: Array<string | undefined> = [];
|
|
106
|
+
const settled: string[] = [];
|
|
107
|
+
const root = new Context();
|
|
108
|
+
await root.plugin(LlmRegistry);
|
|
109
|
+
await mountCredentialRuntime(root, keyringText);
|
|
110
|
+
await root.plugin(
|
|
111
|
+
createOllamaCloudRuntimePlugin({
|
|
112
|
+
accountId: "account-1",
|
|
113
|
+
connectionId: "connection-1",
|
|
114
|
+
packageId: "provider-ollama-cloud",
|
|
115
|
+
now: () => Date.parse("2026-08-30T00:00:00.000Z"),
|
|
116
|
+
leaseCredential: (effectId, expectedGeneration) => {
|
|
117
|
+
leasedGenerations.push(expectedGeneration);
|
|
118
|
+
return Promise.resolve({
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
leaseId: "lease-1",
|
|
121
|
+
effectId,
|
|
122
|
+
connectionId: "connection-1",
|
|
123
|
+
credentialGeneration: "generation-1",
|
|
124
|
+
expiresAt: "2026-08-30T01:00:00.000Z",
|
|
125
|
+
envelope,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
settleCredential: (effectId) => {
|
|
129
|
+
settled.push(effectId);
|
|
130
|
+
return Promise.reject(new Error("settlement unavailable"));
|
|
131
|
+
},
|
|
132
|
+
fetch: (input, init) => {
|
|
133
|
+
const outbound = new Request(input, init);
|
|
134
|
+
authorizations.push(outbound.headers.get("authorization") ?? "");
|
|
135
|
+
return Promise.resolve(
|
|
136
|
+
new Response(
|
|
137
|
+
'data: {"choices":[{"delta":{"content":"hello"}}]}\n\n' +
|
|
138
|
+
'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
|
|
139
|
+
"data: [DONE]\n\n",
|
|
140
|
+
{
|
|
141
|
+
status: 200,
|
|
142
|
+
headers: { "content-type": "text/event-stream" },
|
|
143
|
+
},
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const signal = new AbortController().signal;
|
|
151
|
+
const authorizedRequest = await root.waterfall(
|
|
152
|
+
"agent/request",
|
|
153
|
+
{} as Agent,
|
|
154
|
+
request,
|
|
155
|
+
signal,
|
|
156
|
+
() => Promise.resolve(request),
|
|
157
|
+
);
|
|
158
|
+
expect(leasedGenerations).toEqual([]);
|
|
159
|
+
const events = [];
|
|
160
|
+
for await (const event of root.llm.stream(authorizedRequest, signal)) {
|
|
161
|
+
events.push(event);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
expect(events).toEqual([
|
|
165
|
+
{ type: "text-delta", text: "hello" },
|
|
166
|
+
{ type: "finish", reason: "completed" },
|
|
167
|
+
]);
|
|
168
|
+
expect(authorizations).toEqual(["Bearer account-secret"]);
|
|
169
|
+
expect(leasedGenerations).toEqual(["generation-1"]);
|
|
170
|
+
expect(settled).toEqual([]);
|
|
171
|
+
await expect(
|
|
172
|
+
root.serial(
|
|
173
|
+
"agent/model-outcome-committed",
|
|
174
|
+
{} as Agent,
|
|
175
|
+
request.requestId,
|
|
176
|
+
"completed",
|
|
177
|
+
),
|
|
178
|
+
).rejects.toThrow("settlement unavailable");
|
|
179
|
+
expect(settled).toEqual(["effect-1"]);
|
|
180
|
+
for await (const event of root.llm.stream(authorizedRequest, signal)) {
|
|
181
|
+
void event;
|
|
182
|
+
}
|
|
183
|
+
expect(leasedGenerations).toEqual(["generation-1", "generation-1"]);
|
|
184
|
+
await root.fiber.dispose();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test.each([
|
|
188
|
+
{ effectId: "different-effect", connectionId: "connection-1" },
|
|
189
|
+
{ effectId: "effect-1", connectionId: "connection-2" },
|
|
190
|
+
])(
|
|
191
|
+
"rejects a lease outside the request authority tuple",
|
|
192
|
+
async ({ effectId, connectionId }) => {
|
|
193
|
+
const keyringText = serializedKeyring();
|
|
194
|
+
const envelope = await sealCredentialV1({
|
|
195
|
+
keyring: parseCredentialKeyringV1(keyringText),
|
|
196
|
+
context: {
|
|
197
|
+
accountId: "account-1",
|
|
198
|
+
connectionId: "connection-1",
|
|
199
|
+
packageId: "provider-ollama-cloud",
|
|
200
|
+
credentialGeneration: "generation-1",
|
|
201
|
+
},
|
|
202
|
+
plaintext: "account-secret",
|
|
203
|
+
});
|
|
204
|
+
let openCount = 0;
|
|
205
|
+
const settled: string[] = [];
|
|
206
|
+
const root = new Context();
|
|
207
|
+
await root.plugin(LlmRegistry);
|
|
208
|
+
await mountCredentialRuntime(root, keyringText, () => {
|
|
209
|
+
openCount += 1;
|
|
210
|
+
});
|
|
211
|
+
await root.plugin(
|
|
212
|
+
createOllamaCloudRuntimePlugin({
|
|
213
|
+
accountId: "account-1",
|
|
214
|
+
connectionId: "connection-1",
|
|
215
|
+
packageId: "provider-ollama-cloud",
|
|
216
|
+
now: () => Date.parse("2026-08-30T00:00:00.000Z"),
|
|
217
|
+
leaseCredential: () =>
|
|
218
|
+
Promise.resolve({
|
|
219
|
+
schemaVersion: 1,
|
|
220
|
+
leaseId: "lease-1",
|
|
221
|
+
effectId,
|
|
222
|
+
connectionId,
|
|
223
|
+
credentialGeneration: "generation-1",
|
|
224
|
+
expiresAt: "2026-08-30T01:00:00.000Z",
|
|
225
|
+
envelope,
|
|
226
|
+
}),
|
|
227
|
+
settleCredential: (settledEffectId) => {
|
|
228
|
+
settled.push(settledEffectId);
|
|
229
|
+
return Promise.resolve();
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
let failure: unknown;
|
|
235
|
+
try {
|
|
236
|
+
for await (const event of root.llm.stream(
|
|
237
|
+
request,
|
|
238
|
+
new AbortController().signal,
|
|
239
|
+
)) {
|
|
240
|
+
void event;
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
failure = error;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
expect(failure).toBeInstanceOf(LlmEffectNotStartedError);
|
|
247
|
+
expect(openCount).toBe(0);
|
|
248
|
+
expect(settled).toEqual(["effect-1"]);
|
|
249
|
+
await root.fiber.dispose();
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
test("sends the chat completion to the Connection's endpoint", async () => {
|
|
254
|
+
const keyringText = serializedKeyring();
|
|
255
|
+
const envelope = await sealCredentialV1({
|
|
256
|
+
keyring: parseCredentialKeyringV1(keyringText),
|
|
257
|
+
context: {
|
|
258
|
+
accountId: "account-1",
|
|
259
|
+
connectionId: "connection-1",
|
|
260
|
+
packageId: "provider-ollama-cloud",
|
|
261
|
+
credentialGeneration: "generation-1",
|
|
262
|
+
},
|
|
263
|
+
plaintext: "account-secret",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Unset endpoint keeps the Package default; a Connection setting moves it.
|
|
267
|
+
for (const [apiBaseUrl, expected] of [
|
|
268
|
+
[undefined, "https://ollama.com/v1/chat/completions"],
|
|
269
|
+
["http://127.0.0.1:11434", "http://127.0.0.1:11434/v1/chat/completions"],
|
|
270
|
+
] as const) {
|
|
271
|
+
const urls: string[] = [];
|
|
272
|
+
const root = new Context();
|
|
273
|
+
await root.plugin(LlmRegistry);
|
|
274
|
+
await mountCredentialRuntime(root, keyringText);
|
|
275
|
+
await root.plugin(
|
|
276
|
+
createOllamaCloudRuntimePlugin({
|
|
277
|
+
accountId: "account-1",
|
|
278
|
+
connectionId: "connection-1",
|
|
279
|
+
packageId: "provider-ollama-cloud",
|
|
280
|
+
now: () => Date.parse("2026-08-30T00:00:00.000Z"),
|
|
281
|
+
chatBaseUrl: ollamaChatBaseUrl(apiBaseUrl),
|
|
282
|
+
leaseCredential: (effectId) =>
|
|
283
|
+
Promise.resolve({
|
|
284
|
+
schemaVersion: 1,
|
|
285
|
+
leaseId: "lease-1",
|
|
286
|
+
effectId,
|
|
287
|
+
connectionId: "connection-1",
|
|
288
|
+
credentialGeneration: "generation-1",
|
|
289
|
+
expiresAt: "2026-08-30T01:00:00.000Z",
|
|
290
|
+
envelope,
|
|
291
|
+
}),
|
|
292
|
+
settleCredential: () => Promise.resolve(),
|
|
293
|
+
fetch: (input) => {
|
|
294
|
+
urls.push(String(input));
|
|
295
|
+
return Promise.resolve(
|
|
296
|
+
new Response(
|
|
297
|
+
'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
|
|
298
|
+
"data: [DONE]\n\n",
|
|
299
|
+
{
|
|
300
|
+
status: 200,
|
|
301
|
+
headers: { "content-type": "text/event-stream" },
|
|
302
|
+
},
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const signal = new AbortController().signal;
|
|
310
|
+
for await (const event of root.llm.stream(request, signal)) void event;
|
|
311
|
+
expect(urls).toEqual([expected]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// An unusable endpoint never reaches a request: it is refused at the seam.
|
|
315
|
+
expect(() => ollamaChatBaseUrl("/v1")).toThrow(
|
|
316
|
+
"is not an absolute http or https URL",
|
|
317
|
+
);
|
|
318
|
+
expect(() => ollamaChatBaseUrl("localhost:11434")).toThrow(
|
|
319
|
+
"must use http or https",
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("rejects a request bound to another Connection before leasing", async () => {
|
|
324
|
+
let leaseCount = 0;
|
|
325
|
+
const root = new Context();
|
|
326
|
+
await root.plugin(LlmRegistry);
|
|
327
|
+
await mountCredentialRuntime(root);
|
|
328
|
+
await root.plugin(
|
|
329
|
+
createOllamaCloudRuntimePlugin({
|
|
330
|
+
accountId: "account-1",
|
|
331
|
+
connectionId: "connection-1",
|
|
332
|
+
packageId: "provider-ollama-cloud",
|
|
333
|
+
leaseCredential: () => {
|
|
334
|
+
leaseCount += 1;
|
|
335
|
+
return Promise.reject(new Error("must not lease"));
|
|
336
|
+
},
|
|
337
|
+
settleCredential: () => Promise.resolve(),
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const mismatchedRequest = {
|
|
342
|
+
...request,
|
|
343
|
+
modelBinding: {
|
|
344
|
+
...request.modelBinding,
|
|
345
|
+
connectionId: "connection-2",
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
let failure: unknown;
|
|
349
|
+
try {
|
|
350
|
+
for await (const event of root.llm.stream(
|
|
351
|
+
mismatchedRequest,
|
|
352
|
+
new AbortController().signal,
|
|
353
|
+
)) {
|
|
354
|
+
void event;
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
failure = error;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
expect(failure).toBeInstanceOf(LlmEffectNotStartedError);
|
|
361
|
+
expect(leaseCount).toBe(0);
|
|
362
|
+
await root.fiber.dispose();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("settles a durable outcome after provider reconstruction", async () => {
|
|
366
|
+
const settled: string[] = [];
|
|
367
|
+
const root = new Context();
|
|
368
|
+
await root.plugin(LlmRegistry);
|
|
369
|
+
await mountCredentialRuntime(root);
|
|
370
|
+
await root.plugin(
|
|
371
|
+
createOllamaCloudRuntimePlugin({
|
|
372
|
+
accountId: "account-1",
|
|
373
|
+
connectionId: "connection-1",
|
|
374
|
+
packageId: "provider-ollama-cloud",
|
|
375
|
+
leaseCredential: () => Promise.reject(new Error("not used")),
|
|
376
|
+
settleCredential: (effectId) => {
|
|
377
|
+
settled.push(effectId);
|
|
378
|
+
return Promise.resolve();
|
|
379
|
+
},
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
await root.serial(
|
|
384
|
+
"agent/model-outcome-committed",
|
|
385
|
+
{} as Agent,
|
|
386
|
+
"durable-effect",
|
|
387
|
+
"completed",
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
expect(settled).toEqual(["durable-effect"]);
|
|
391
|
+
await root.fiber.dispose();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test.each([401, 403, 404])(
|
|
395
|
+
"settles definitive HTTP %i rejections after durable no-effect outcome",
|
|
396
|
+
async (status) => {
|
|
397
|
+
const keyringText = serializedKeyring();
|
|
398
|
+
const envelope = await sealCredentialV1({
|
|
399
|
+
keyring: parseCredentialKeyringV1(keyringText),
|
|
400
|
+
context: {
|
|
401
|
+
accountId: "account-1",
|
|
402
|
+
connectionId: "connection-1",
|
|
403
|
+
packageId: "provider-ollama-cloud",
|
|
404
|
+
credentialGeneration: "generation-1",
|
|
405
|
+
},
|
|
406
|
+
plaintext: "account-secret",
|
|
407
|
+
});
|
|
408
|
+
const settled: string[] = [];
|
|
409
|
+
const root = new Context();
|
|
410
|
+
await root.plugin(LlmRegistry);
|
|
411
|
+
await mountCredentialRuntime(root, keyringText);
|
|
412
|
+
await root.plugin(
|
|
413
|
+
createOllamaCloudRuntimePlugin({
|
|
414
|
+
accountId: "account-1",
|
|
415
|
+
connectionId: "connection-1",
|
|
416
|
+
packageId: "provider-ollama-cloud",
|
|
417
|
+
now: () => Date.parse("2026-08-30T00:00:00.000Z"),
|
|
418
|
+
leaseCredential: (effectId) =>
|
|
419
|
+
Promise.resolve({
|
|
420
|
+
schemaVersion: 1,
|
|
421
|
+
leaseId: "lease-1",
|
|
422
|
+
effectId,
|
|
423
|
+
connectionId: "connection-1",
|
|
424
|
+
credentialGeneration: "generation-1",
|
|
425
|
+
expiresAt: "2026-08-30T01:00:00.000Z",
|
|
426
|
+
envelope,
|
|
427
|
+
}),
|
|
428
|
+
settleCredential: (effectId) => {
|
|
429
|
+
settled.push(effectId);
|
|
430
|
+
return Promise.resolve();
|
|
431
|
+
},
|
|
432
|
+
fetch: () =>
|
|
433
|
+
Promise.resolve(new Response("definitive rejection", { status })),
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
let failure: unknown;
|
|
438
|
+
try {
|
|
439
|
+
for await (const event of root.llm.stream(
|
|
440
|
+
request,
|
|
441
|
+
new AbortController().signal,
|
|
442
|
+
)) {
|
|
443
|
+
void event;
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
failure = error;
|
|
447
|
+
}
|
|
448
|
+
expect(failure).toBeInstanceOf(LlmEffectNotStartedError);
|
|
449
|
+
expect(settled).toEqual([]);
|
|
450
|
+
await root.serial(
|
|
451
|
+
"agent/model-outcome-committed",
|
|
452
|
+
{} as Agent,
|
|
453
|
+
request.requestId,
|
|
454
|
+
"not-started",
|
|
455
|
+
);
|
|
456
|
+
expect(settled).toEqual(["effect-1"]);
|
|
457
|
+
await root.fiber.dispose();
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
test("requires reconciliation for ambiguous HTTP failures", async () => {
|
|
462
|
+
const keyringText = serializedKeyring();
|
|
463
|
+
const envelope = await sealCredentialV1({
|
|
464
|
+
keyring: parseCredentialKeyringV1(keyringText),
|
|
465
|
+
context: {
|
|
466
|
+
accountId: "account-1",
|
|
467
|
+
connectionId: "connection-1",
|
|
468
|
+
packageId: "provider-ollama-cloud",
|
|
469
|
+
credentialGeneration: "generation-1",
|
|
470
|
+
},
|
|
471
|
+
plaintext: "account-secret",
|
|
472
|
+
});
|
|
473
|
+
const settled: string[] = [];
|
|
474
|
+
const root = new Context();
|
|
475
|
+
await root.plugin(LlmRegistry);
|
|
476
|
+
await mountCredentialRuntime(root, keyringText);
|
|
477
|
+
await root.plugin(
|
|
478
|
+
createOllamaCloudRuntimePlugin({
|
|
479
|
+
accountId: "account-1",
|
|
480
|
+
connectionId: "connection-1",
|
|
481
|
+
packageId: "provider-ollama-cloud",
|
|
482
|
+
now: () => Date.parse("2026-08-30T00:00:00.000Z"),
|
|
483
|
+
leaseCredential: (effectId) =>
|
|
484
|
+
Promise.resolve({
|
|
485
|
+
schemaVersion: 1,
|
|
486
|
+
leaseId: "lease-1",
|
|
487
|
+
effectId,
|
|
488
|
+
connectionId: "connection-1",
|
|
489
|
+
credentialGeneration: "generation-1",
|
|
490
|
+
expiresAt: "2026-08-30T01:00:00.000Z",
|
|
491
|
+
envelope,
|
|
492
|
+
}),
|
|
493
|
+
settleCredential: (effectId) => {
|
|
494
|
+
settled.push(effectId);
|
|
495
|
+
return Promise.resolve();
|
|
496
|
+
},
|
|
497
|
+
fetch: () => Promise.resolve(new Response("timeout", { status: 408 })),
|
|
498
|
+
}),
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
let failure: unknown;
|
|
502
|
+
try {
|
|
503
|
+
for await (const _ of root.llm.stream(
|
|
504
|
+
request,
|
|
505
|
+
new AbortController().signal,
|
|
506
|
+
)) {
|
|
507
|
+
void _;
|
|
508
|
+
}
|
|
509
|
+
} catch (error) {
|
|
510
|
+
failure = error;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
expect(failure).toBeInstanceOf(OpenAICompatibleHttpError);
|
|
514
|
+
expect(failure).not.toBeInstanceOf(LlmEffectNotStartedError);
|
|
515
|
+
expect(settled).toEqual([]);
|
|
516
|
+
await root.fiber.dispose();
|
|
517
|
+
});
|
|
518
|
+
});
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LlmEffectNotStartedError,
|
|
3
|
+
type LlmProvider,
|
|
4
|
+
type NormalizedModelRequest,
|
|
5
|
+
} from "@frockbot/kernel-contracts";
|
|
6
|
+
import { type Agent } from "@frockbot/kernel-agent-loop/agent";
|
|
7
|
+
import type { CredentialLeaseV1 } from "@frockbot/connection-core";
|
|
8
|
+
import {
|
|
9
|
+
OpenAICompatibleHttpError,
|
|
10
|
+
OpenAICompatibleProvider,
|
|
11
|
+
} from "@frockbot/provider-openai-compatible";
|
|
12
|
+
import type { Plugin } from "cordis";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_OLLAMA_API_BASE_URL,
|
|
15
|
+
decodeOllamaApiBaseUrl,
|
|
16
|
+
type OllamaFetch,
|
|
17
|
+
} from "./client.js";
|
|
18
|
+
|
|
19
|
+
interface CredentialLeaseOpener {
|
|
20
|
+
open(input: {
|
|
21
|
+
accountId: string;
|
|
22
|
+
connectionId: string;
|
|
23
|
+
packageId: string;
|
|
24
|
+
lease: CredentialLeaseV1;
|
|
25
|
+
}): Promise<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module "cordis" {
|
|
29
|
+
interface Context {
|
|
30
|
+
credentialLease: CredentialLeaseOpener;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Events {
|
|
34
|
+
"agent/model-outcome-committed": (
|
|
35
|
+
agent: Agent,
|
|
36
|
+
requestId: string,
|
|
37
|
+
outcome: "completed" | "not-started",
|
|
38
|
+
) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const OLLAMA_CLOUD_PROVIDER = "ollama-cloud";
|
|
43
|
+
|
|
44
|
+
export interface OllamaCloudRuntimeConfig {
|
|
45
|
+
accountId: string;
|
|
46
|
+
connectionId: string;
|
|
47
|
+
packageId: "provider-ollama-cloud";
|
|
48
|
+
leaseCredential(
|
|
49
|
+
effectId: string,
|
|
50
|
+
expectedGeneration?: string,
|
|
51
|
+
): Promise<CredentialLeaseV1>;
|
|
52
|
+
settleCredential(effectId: string): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* OpenAI-compatible chat root for this Connection's endpoint. Defaults to
|
|
55
|
+
* the Package endpoint `https://ollama.com/v1`; a Connection that points
|
|
56
|
+
* elsewhere supplies `<apiBaseUrl>/v1`.
|
|
57
|
+
*/
|
|
58
|
+
chatBaseUrl?: string;
|
|
59
|
+
fetch?: OllamaFetch;
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Compose the OpenAI-compatible chat root from a Connection endpoint root. */
|
|
64
|
+
export function ollamaChatBaseUrl(apiBaseUrl?: string): string {
|
|
65
|
+
return `${decodeOllamaApiBaseUrl(apiBaseUrl ?? DEFAULT_OLLAMA_API_BASE_URL)}/v1`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AuthorizedRequest {
|
|
69
|
+
lease: CredentialLeaseV1;
|
|
70
|
+
apiKey: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class OllamaCloudProvider implements LlmProvider {
|
|
74
|
+
readonly id = OLLAMA_CLOUD_PROVIDER;
|
|
75
|
+
private readonly authorized = new Map<string, AuthorizedRequest>();
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
private readonly config: OllamaCloudRuntimeConfig,
|
|
79
|
+
private readonly credentialLease: CredentialLeaseOpener,
|
|
80
|
+
) {}
|
|
81
|
+
|
|
82
|
+
async authorize(request: NormalizedModelRequest): Promise<void> {
|
|
83
|
+
const binding = request.modelBinding;
|
|
84
|
+
const expectedGeneration = binding?.connectionGeneration;
|
|
85
|
+
if (
|
|
86
|
+
!expectedGeneration ||
|
|
87
|
+
binding.connectionId !== this.config.connectionId
|
|
88
|
+
) {
|
|
89
|
+
throw new LlmEffectNotStartedError(
|
|
90
|
+
"Ollama Cloud request has invalid Connection authority",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const existing = this.authorized.get(request.requestId);
|
|
94
|
+
if (existing) {
|
|
95
|
+
if (existing.lease.credentialGeneration !== expectedGeneration) {
|
|
96
|
+
throw new LlmEffectNotStartedError(
|
|
97
|
+
"Ollama Cloud request generation changed",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let lease: CredentialLeaseV1 | undefined;
|
|
104
|
+
try {
|
|
105
|
+
lease = await this.config.leaseCredential(
|
|
106
|
+
request.requestId,
|
|
107
|
+
expectedGeneration,
|
|
108
|
+
);
|
|
109
|
+
if (
|
|
110
|
+
lease.effectId !== request.requestId ||
|
|
111
|
+
lease.connectionId !== this.config.connectionId ||
|
|
112
|
+
lease.credentialGeneration !== expectedGeneration ||
|
|
113
|
+
Date.parse(lease.expiresAt) <= (this.config.now ?? Date.now)()
|
|
114
|
+
) {
|
|
115
|
+
throw new Error("Ollama Cloud credential lease is invalid");
|
|
116
|
+
}
|
|
117
|
+
const apiKey = await this.credentialLease.open({
|
|
118
|
+
accountId: this.config.accountId,
|
|
119
|
+
connectionId: this.config.connectionId,
|
|
120
|
+
packageId: this.config.packageId,
|
|
121
|
+
lease,
|
|
122
|
+
});
|
|
123
|
+
this.authorized.set(request.requestId, { lease, apiKey });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (lease) {
|
|
126
|
+
await this.config
|
|
127
|
+
.settleCredential(request.requestId)
|
|
128
|
+
.catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
throw new LlmEffectNotStartedError(
|
|
131
|
+
error instanceof Error
|
|
132
|
+
? error.message
|
|
133
|
+
: "Ollama Cloud credential is unavailable",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async settle(requestId: string): Promise<void> {
|
|
139
|
+
try {
|
|
140
|
+
await this.config.settleCredential(requestId);
|
|
141
|
+
} finally {
|
|
142
|
+
this.authorized.delete(requestId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async *stream(request: NormalizedModelRequest, signal: AbortSignal) {
|
|
147
|
+
await this.authorize(request);
|
|
148
|
+
const authorization = this.authorized.get(request.requestId);
|
|
149
|
+
if (!authorization) {
|
|
150
|
+
throw new LlmEffectNotStartedError(
|
|
151
|
+
"Ollama Cloud request authorization is unavailable",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const provider = new OpenAICompatibleProvider({
|
|
155
|
+
baseUrl: this.config.chatBaseUrl ?? ollamaChatBaseUrl(),
|
|
156
|
+
apiKey: authorization.apiKey,
|
|
157
|
+
providerId: this.id,
|
|
158
|
+
fetch: this.config.fetch,
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
yield* provider.stream(request, signal);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (
|
|
164
|
+
error instanceof OpenAICompatibleHttpError &&
|
|
165
|
+
(error.status === 401 || error.status === 403 || error.status === 404)
|
|
166
|
+
) {
|
|
167
|
+
throw new LlmEffectNotStartedError(error.message);
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createOllamaCloudRuntimePlugin(
|
|
175
|
+
config: OllamaCloudRuntimeConfig,
|
|
176
|
+
): Plugin.Function {
|
|
177
|
+
const plugin: Plugin.Function = (ctx) => {
|
|
178
|
+
const provider = new OllamaCloudProvider(config, ctx.credentialLease);
|
|
179
|
+
const disposeProvider = ctx.llm.register(provider);
|
|
180
|
+
const disposeSettlement = ctx.on(
|
|
181
|
+
"agent/model-outcome-committed",
|
|
182
|
+
async (_agent, requestId) => provider.settle(requestId),
|
|
183
|
+
);
|
|
184
|
+
return () => {
|
|
185
|
+
disposeSettlement();
|
|
186
|
+
disposeProvider();
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
plugin.inject = ["llm", "credentialLease"];
|
|
190
|
+
return plugin;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default createOllamaCloudRuntimePlugin;
|