offrouter-core 0.2.1 → 0.2.2

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.
Files changed (139) hide show
  1. package/dist/audit.d.ts +137 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +320 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/auth/credential-chain.d.ts +84 -0
  6. package/dist/auth/credential-chain.d.ts.map +1 -0
  7. package/dist/auth/credential-chain.js +150 -0
  8. package/dist/auth/credential-chain.js.map +1 -0
  9. package/dist/auth/index.d.ts +10 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +7 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/keychain.d.ts +60 -0
  14. package/dist/auth/keychain.d.ts.map +1 -0
  15. package/dist/auth/keychain.js +104 -0
  16. package/dist/auth/keychain.js.map +1 -0
  17. package/dist/auth/oauth-pkce.d.ts +138 -0
  18. package/dist/auth/oauth-pkce.d.ts.map +1 -0
  19. package/dist/auth/oauth-pkce.js +375 -0
  20. package/dist/auth/oauth-pkce.js.map +1 -0
  21. package/dist/auth/oauth-server.d.ts +36 -0
  22. package/dist/auth/oauth-server.d.ts.map +1 -0
  23. package/dist/auth/oauth-server.js +210 -0
  24. package/dist/auth/oauth-server.js.map +1 -0
  25. package/dist/config.d.ts +76 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +355 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/delegation.d.ts +123 -0
  30. package/dist/delegation.d.ts.map +1 -0
  31. package/dist/delegation.js +455 -0
  32. package/dist/delegation.js.map +1 -0
  33. package/dist/index.d.ts +41 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +26 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/policy.d.ts +45 -0
  38. package/dist/policy.d.ts.map +1 -0
  39. package/dist/policy.js +318 -0
  40. package/dist/policy.js.map +1 -0
  41. package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +6 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/providers/adapter.d.ts +137 -0
  46. package/dist/providers/adapter.d.ts.map +1 -0
  47. package/dist/providers/adapter.js +163 -0
  48. package/dist/providers/adapter.js.map +1 -0
  49. package/dist/providers/catalog-data.d.ts +128 -0
  50. package/dist/providers/catalog-data.d.ts.map +1 -0
  51. package/dist/providers/catalog-data.js +397 -0
  52. package/dist/providers/catalog-data.js.map +1 -0
  53. package/dist/providers/catalog.d.ts +63 -0
  54. package/dist/providers/catalog.d.ts.map +1 -0
  55. package/dist/providers/catalog.js +394 -0
  56. package/dist/providers/catalog.js.map +1 -0
  57. package/dist/providers/fake.d.ts +46 -0
  58. package/dist/providers/fake.d.ts.map +1 -0
  59. package/dist/providers/fake.js +234 -0
  60. package/dist/providers/fake.js.map +1 -0
  61. package/dist/providers/openai-compatible.d.ts +65 -0
  62. package/dist/providers/openai-compatible.d.ts.map +1 -0
  63. package/dist/providers/openai-compatible.js +434 -0
  64. package/dist/providers/openai-compatible.js.map +1 -0
  65. package/dist/proxy/responses-mapper.d.ts +366 -0
  66. package/dist/proxy/responses-mapper.d.ts.map +1 -0
  67. package/dist/proxy/responses-mapper.js +517 -0
  68. package/dist/proxy/responses-mapper.js.map +1 -0
  69. package/dist/proxy/responses-server.d.ts +29 -0
  70. package/dist/proxy/responses-server.d.ts.map +1 -0
  71. package/dist/proxy/responses-server.js +360 -0
  72. package/dist/proxy/responses-server.js.map +1 -0
  73. package/dist/router.d.ts +18 -0
  74. package/dist/router.d.ts.map +1 -0
  75. package/dist/router.js +296 -0
  76. package/dist/router.js.map +1 -0
  77. package/dist/schemas.d.ts +560 -0
  78. package/dist/schemas.d.ts.map +1 -0
  79. package/{src/schemas.ts → dist/schemas.js} +83 -103
  80. package/dist/schemas.js.map +1 -0
  81. package/dist/secrets.d.ts +112 -0
  82. package/dist/secrets.d.ts.map +1 -0
  83. package/dist/secrets.js +326 -0
  84. package/dist/secrets.js.map +1 -0
  85. package/dist/types.d.ts +117 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +6 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/usage-file.d.ts +59 -0
  90. package/dist/usage-file.d.ts.map +1 -0
  91. package/dist/usage-file.js +316 -0
  92. package/dist/usage-file.js.map +1 -0
  93. package/dist/usage.d.ts +235 -0
  94. package/dist/usage.d.ts.map +1 -0
  95. package/dist/usage.js +517 -0
  96. package/dist/usage.js.map +1 -0
  97. package/package.json +7 -2
  98. package/src/audit.test.ts +0 -302
  99. package/src/audit.ts +0 -553
  100. package/src/auth/credential-chain.test.ts +0 -326
  101. package/src/auth/credential-chain.ts +0 -235
  102. package/src/auth/index.ts +0 -45
  103. package/src/auth/keychain.test.ts +0 -265
  104. package/src/auth/keychain.ts +0 -168
  105. package/src/auth/oauth-pkce.test.ts +0 -329
  106. package/src/auth/oauth-pkce.ts +0 -571
  107. package/src/auth/oauth-server.test.ts +0 -83
  108. package/src/auth/oauth-server.ts +0 -296
  109. package/src/config.test.ts +0 -479
  110. package/src/config.ts +0 -505
  111. package/src/delegation.test.ts +0 -368
  112. package/src/delegation.ts +0 -605
  113. package/src/index.ts +0 -280
  114. package/src/policy.test.ts +0 -634
  115. package/src/policy.ts +0 -420
  116. package/src/providers/adapter.test.ts +0 -293
  117. package/src/providers/adapter.ts +0 -328
  118. package/src/providers/catalog-data.test.ts +0 -258
  119. package/src/providers/catalog-data.ts +0 -498
  120. package/src/providers/catalog.test.ts +0 -84
  121. package/src/providers/catalog.ts +0 -483
  122. package/src/providers/fake.ts +0 -312
  123. package/src/providers/openai-compatible.test.ts +0 -366
  124. package/src/providers/openai-compatible.ts +0 -554
  125. package/src/proxy/responses-mapper.test.ts +0 -290
  126. package/src/proxy/responses-mapper.ts +0 -736
  127. package/src/proxy/responses-server.test.ts +0 -322
  128. package/src/proxy/responses-server.ts +0 -469
  129. package/src/router.test.ts +0 -699
  130. package/src/router.ts +0 -352
  131. package/src/schemas.test.ts +0 -291
  132. package/src/secrets.test.ts +0 -271
  133. package/src/secrets.ts +0 -461
  134. package/src/types.ts +0 -173
  135. package/src/usage-file.test.ts +0 -243
  136. package/src/usage-file.ts +0 -435
  137. package/src/usage.test.ts +0 -335
  138. package/src/usage.ts +0 -859
  139. package/tsconfig.json +0 -9
package/src/delegation.ts DELETED
@@ -1,605 +0,0 @@
1
- /**
2
- * Minimal in-process delegation runtime for OffRouter V1.
3
- *
4
- * - Routes with digest/preview only until policy allows a provider.
5
- * - Full prompt text is sent to ProviderAdapter only after allow.
6
- * - Active tasks live in memory in this process.
7
- * - Cancellation only affects active same-process tasks.
8
- */
9
- import { randomBytes } from "node:crypto";
10
- import { InMemoryAuditStore, type AuditStore } from "./audit.js";
11
- import type { PolicyConfig } from "./policy.js";
12
- import type { ProviderAdapter } from "./providers/adapter.js";
13
- import { isProviderError } from "./providers/adapter.js";
14
- import { routeRequest } from "./router.js";
15
- import type {
16
- PolicyDenial,
17
- ProviderCandidate,
18
- RouteDecision,
19
- RouteRequest,
20
- } from "./types.js";
21
-
22
- export type DelegationMode = "oneshot" | "stream" | "background";
23
- export type DelegationOutput = "text" | "json" | "patch";
24
- export type DelegationTaskStatus =
25
- "queued" | "running" | "completed" | "cancelled" | "failed";
26
-
27
- /**
28
- * Delegation carries full prompt text only after the caller is past policy.
29
- * The runtime still re-checks policy before invoke and rejects when blocked.
30
- */
31
- export interface DelegationRequest {
32
- taskId: string;
33
- mode: DelegationMode;
34
- output: DelegationOutput;
35
- /** Full user prompt; never audited raw. */
36
- prompt: string;
37
- route: RouteRequest;
38
- model?: string;
39
- providerId?: string;
40
- maxOutputTokens?: number;
41
- temperature?: number;
42
- }
43
-
44
- export type DelegationEvent =
45
- | { type: "start"; taskId: string; callId: string; model: string }
46
- | { type: "text-delta"; taskId: string; callId: string; text: string }
47
- | {
48
- type: "usage";
49
- taskId: string;
50
- callId: string;
51
- inputTokens?: number;
52
- outputTokens?: number;
53
- totalTokens?: number;
54
- }
55
- | {
56
- type: "done";
57
- taskId: string;
58
- callId: string;
59
- finishReason?: string;
60
- text?: string;
61
- }
62
- | {
63
- type: "error";
64
- taskId: string;
65
- callId?: string;
66
- message: string;
67
- code?: string;
68
- }
69
- | { type: "cancelled"; taskId: string; callId?: string };
70
-
71
- export interface DelegationTask {
72
- taskId: string;
73
- callId: string;
74
- status: DelegationTaskStatus;
75
- mode: DelegationMode;
76
- requestId: string;
77
- providerId?: string;
78
- model?: string;
79
- createdAt: string;
80
- updatedAt: string;
81
- text?: string;
82
- error?: string;
83
- events: DelegationEvent[];
84
- decision?: RouteDecision;
85
- }
86
-
87
- export interface DelegationResult {
88
- taskId: string;
89
- status: DelegationTaskStatus;
90
- text?: string;
91
- error?: string;
92
- events: DelegationEvent[];
93
- decision?: RouteDecision;
94
- }
95
-
96
- export interface DelegationRuntimeOptions {
97
- adapters: Record<string, ProviderAdapter>;
98
- candidates: ProviderCandidate[];
99
- policy: PolicyConfig;
100
- audit?: AuditStore;
101
- now?: () => Date;
102
- /** Default true: store redacted prompt preview on audit append. */
103
- includePromptPreview?: boolean;
104
- }
105
-
106
- export class DelegationPolicyError extends Error {
107
- readonly decision: RouteDecision;
108
-
109
- constructor(message: string, decision: RouteDecision) {
110
- super(message);
111
- this.name = "DelegationPolicyError";
112
- this.decision = decision;
113
- }
114
- }
115
-
116
- function newCallId(): string {
117
- return `call_${randomBytes(8).toString("hex")}`;
118
- }
119
-
120
- export class DelegationRuntime {
121
- private readonly adapters: Record<string, ProviderAdapter>;
122
- private readonly candidates: ProviderCandidate[];
123
- private readonly policy: PolicyConfig;
124
- private readonly audit: AuditStore;
125
- private readonly now: () => Date;
126
- private readonly includePromptPreview: boolean;
127
- private readonly tasks = new Map<string, DelegationTask>();
128
- private readonly controllers = new Map<string, AbortController>();
129
- private readonly waiters = new Map<string, Promise<DelegationTask>>();
130
-
131
- constructor(options: DelegationRuntimeOptions) {
132
- this.adapters = options.adapters;
133
- this.candidates = options.candidates;
134
- this.policy = options.policy;
135
- this.audit = options.audit ?? new InMemoryAuditStore();
136
- this.now = options.now ?? (() => new Date());
137
- this.includePromptPreview = options.includePromptPreview ?? true;
138
- }
139
-
140
- getTask(taskId: string): DelegationTask | undefined {
141
- const t = this.tasks.get(taskId);
142
- return t ? cloneTask(t) : undefined;
143
- }
144
-
145
- listTasks(): DelegationTask[] {
146
- return [...this.tasks.values()].map(cloneTask);
147
- }
148
-
149
- /**
150
- * One-shot: run to completion and return aggregated text + events.
151
- */
152
- async delegate(request: DelegationRequest): Promise<DelegationResult> {
153
- const events: DelegationEvent[] = [];
154
- for await (const event of this.#run(request, "oneshot")) {
155
- events.push(event);
156
- }
157
- const task = this.tasks.get(request.taskId);
158
- if (!task) {
159
- throw new Error(`delegation task ${request.taskId} missing after run`);
160
- }
161
- if (task.status === "failed") {
162
- // still return a result object for inspectability when policy error throws
163
- }
164
- return {
165
- taskId: task.taskId,
166
- status: task.status,
167
- text: task.text,
168
- error: task.error,
169
- events: [...task.events],
170
- decision: task.decision,
171
- };
172
- }
173
-
174
- /**
175
- * Streaming async iterator of DelegationEvents.
176
- */
177
- async *stream(
178
- request: DelegationRequest,
179
- ): AsyncGenerator<DelegationEvent, void, undefined> {
180
- yield* this.#run(request, "stream");
181
- }
182
-
183
- /**
184
- * Start background work; returns immediately with a running/queued task.
185
- */
186
- async startBackground(request: DelegationRequest): Promise<DelegationTask> {
187
- const existing = this.tasks.get(request.taskId);
188
- if (existing && existing.status === "running") {
189
- return cloneTask(existing);
190
- }
191
-
192
- const stamped = this.#stampTask(request, "background");
193
- this.tasks.set(request.taskId, stamped);
194
-
195
- let releaseRunning!: () => void;
196
- const runningGate = new Promise<void>((resolve) => {
197
- releaseRunning = resolve;
198
- });
199
-
200
- const runPromise = (async () => {
201
- try {
202
- for await (const event of this.#run(request, "background", {
203
- preRegistered: true,
204
- onRunning: releaseRunning,
205
- })) {
206
- void event;
207
- // drain
208
- }
209
- } catch {
210
- // policy/provider failure already reflected on task; still open the gate
211
- releaseRunning();
212
- } finally {
213
- releaseRunning();
214
- }
215
- return this.tasks.get(request.taskId)!;
216
- })();
217
-
218
- this.waiters.set(request.taskId, runPromise);
219
- // Wait until policy passed (or failed) and task entered a terminal/running state.
220
- await runningGate;
221
- const current = this.tasks.get(request.taskId);
222
- return cloneTask(current ?? stamped);
223
- }
224
-
225
- async wait(taskId: string): Promise<DelegationTask> {
226
- const pending = this.waiters.get(taskId);
227
- if (pending) {
228
- const task = await pending;
229
- return cloneTask(task);
230
- }
231
- const task = this.tasks.get(taskId);
232
- if (!task) {
233
- throw new Error(`unknown delegation task ${taskId}`);
234
- }
235
- if (
236
- task.status === "completed" ||
237
- task.status === "cancelled" ||
238
- task.status === "failed"
239
- ) {
240
- return cloneTask(task);
241
- }
242
- // Still running without waiter - poll briefly
243
- for (let i = 0; i < 200; i++) {
244
- await sleep(10);
245
- const t = this.tasks.get(taskId);
246
- if (
247
- t &&
248
- (t.status === "completed" ||
249
- t.status === "cancelled" ||
250
- t.status === "failed")
251
- ) {
252
- return cloneTask(t);
253
- }
254
- }
255
- throw new Error(`timeout waiting for task ${taskId}`);
256
- }
257
-
258
- async cancel(taskId: string): Promise<DelegationTask | undefined> {
259
- const task = this.tasks.get(taskId);
260
- if (!task) return undefined;
261
-
262
- if (
263
- task.status === "completed" ||
264
- task.status === "cancelled" ||
265
- task.status === "failed"
266
- ) {
267
- return cloneTask(task);
268
- }
269
-
270
- this.controllers.get(taskId)?.abort();
271
- const adapter =
272
- task.providerId != null ? this.adapters[task.providerId] : undefined;
273
- if (adapter && task.callId) {
274
- await adapter.cancel(task.callId);
275
- }
276
-
277
- task.status = "cancelled";
278
- task.updatedAt = this.now().toISOString();
279
- const event: DelegationEvent = {
280
- type: "cancelled",
281
- taskId,
282
- callId: task.callId,
283
- };
284
- task.events.push(event);
285
-
286
- await this.audit.markCancelled(task.requestId, {
287
- reason: "delegation_cancelled",
288
- now: this.now,
289
- });
290
-
291
- return cloneTask(task);
292
- }
293
-
294
- async *#run(
295
- request: DelegationRequest,
296
- forcedMode: DelegationMode,
297
- options: { preRegistered?: boolean; onRunning?: () => void } = {},
298
- ): AsyncGenerator<DelegationEvent, void, undefined> {
299
- const mode = forcedMode;
300
- const taskId = request.taskId;
301
- let task = this.tasks.get(taskId);
302
-
303
- if (!options.preRegistered || !task) {
304
- task = this.#stampTask(request, mode);
305
- this.tasks.set(taskId, task);
306
- } else {
307
- task.mode = mode;
308
- }
309
-
310
- // Policy/routing with digest-only route request - do not rely on full prompt.
311
- let decision = routeRequest(request.route, this.candidates, this.policy);
312
- decision = applyRouteOverrideDenial(request, decision);
313
- task.decision = decision;
314
-
315
- await this.audit.appendDecision(request.route, decision, {
316
- includePromptPreview: this.includePromptPreview,
317
- now: this.now,
318
- });
319
-
320
- if (decision.blocked || !decision.route) {
321
- task.status = "failed";
322
- task.error =
323
- decision.explanation || decision.reason || "policy blocked delegation";
324
- task.updatedAt = this.now().toISOString();
325
- const event: DelegationEvent = {
326
- type: "error",
327
- taskId,
328
- message: task.error,
329
- code: "policy_blocked",
330
- };
331
- task.events.push(event);
332
- options.onRunning?.();
333
- yield event;
334
- throw new DelegationPolicyError(task.error, decision);
335
- }
336
-
337
- const providerId = decision.route.provider;
338
- const model = decision.route.model;
339
- const adapter = this.adapters[providerId];
340
- if (!adapter) {
341
- task.status = "failed";
342
- task.error = `no adapter registered for provider ${providerId}`;
343
- task.updatedAt = this.now().toISOString();
344
- const event: DelegationEvent = {
345
- type: "error",
346
- taskId,
347
- message: task.error,
348
- code: "adapter_missing",
349
- };
350
- task.events.push(event);
351
- yield event;
352
- throw new Error(task.error);
353
- }
354
-
355
- const callId = task.callId || newCallId();
356
- task.callId = callId;
357
- task.providerId = providerId;
358
- task.model = model;
359
- task.status = "running";
360
- task.updatedAt = this.now().toISOString();
361
- options.onRunning?.();
362
-
363
- const controller = new AbortController();
364
- this.controllers.set(taskId, controller);
365
-
366
- if (controller.signal.aborted || statusOf(task) === "cancelled") {
367
- yield* this.#markCancelled(task);
368
- return;
369
- }
370
-
371
- const textParts: string[] = [];
372
- try {
373
- // Full prompt is only forwarded after policy allowed the provider.
374
- for await (const pev of adapter.invoke({
375
- callId,
376
- model,
377
- messages: [{ role: "user", content: request.prompt }],
378
- stream: mode !== "oneshot",
379
- maxOutputTokens: request.maxOutputTokens,
380
- temperature: request.temperature,
381
- signal: controller.signal,
382
- })) {
383
- // Honor mid-flight cancellation.
384
- if (statusOf(task) === "cancelled" || controller.signal.aborted) {
385
- yield* this.#markCancelled(task);
386
- return;
387
- }
388
-
389
- const mapped = mapProviderEvent(taskId, pev);
390
- if (mapped) {
391
- task.events.push(mapped);
392
- if (mapped.type === "text-delta") {
393
- textParts.push(mapped.text);
394
- }
395
- yield mapped;
396
- if (mapped.type === "error") {
397
- if (mapped.code === "cancelled") {
398
- task.status = "cancelled";
399
- task.updatedAt = this.now().toISOString();
400
- await this.audit.markCancelled(task.requestId, {
401
- reason: "provider_cancelled",
402
- now: this.now,
403
- });
404
- return;
405
- }
406
- task.status = "failed";
407
- task.error = mapped.message;
408
- task.updatedAt = this.now().toISOString();
409
- return;
410
- }
411
- if (mapped.type === "done") {
412
- task.text = textParts.join("");
413
- task.status = "completed";
414
- task.updatedAt = this.now().toISOString();
415
- await this.audit.markCompleted(task.requestId, { now: this.now });
416
- return;
417
- }
418
- }
419
- }
420
-
421
- // Exhausted without done
422
- if (statusOf(task) === "running") {
423
- task.text = textParts.join("");
424
- task.status = "completed";
425
- task.updatedAt = this.now().toISOString();
426
- const done: DelegationEvent = {
427
- type: "done",
428
- taskId,
429
- callId,
430
- text: task.text,
431
- };
432
- task.events.push(done);
433
- yield done;
434
- await this.audit.markCompleted(task.requestId, { now: this.now });
435
- }
436
- } catch (err) {
437
- if (statusOf(task) === "cancelled" || controller.signal.aborted) {
438
- yield* this.#markCancelled(task);
439
- return;
440
- }
441
- if (isProviderError(err) && err.code === "cancelled") {
442
- yield* this.#markCancelled(task);
443
- return;
444
- }
445
- const message =
446
- err instanceof Error ? err.message : "provider invoke failed";
447
- task.status = "failed";
448
- task.error = message;
449
- task.updatedAt = this.now().toISOString();
450
- const event: DelegationEvent = {
451
- type: "error",
452
- taskId,
453
- callId,
454
- message,
455
- code: isProviderError(err) ? err.code : "unknown",
456
- };
457
- task.events.push(event);
458
- yield event;
459
- } finally {
460
- this.controllers.delete(taskId);
461
- }
462
- }
463
-
464
- #stampTask(request: DelegationRequest, mode: DelegationMode): DelegationTask {
465
- const ts = this.now().toISOString();
466
- return {
467
- taskId: request.taskId,
468
- callId: newCallId(),
469
- status: "queued",
470
- mode,
471
- requestId: request.route.requestId,
472
- createdAt: ts,
473
- updatedAt: ts,
474
- events: [],
475
- };
476
- }
477
-
478
- async *#markCancelled(
479
- task: DelegationTask,
480
- ): AsyncGenerator<DelegationEvent, void, undefined> {
481
- if (task.status !== "cancelled") {
482
- task.status = "cancelled";
483
- task.updatedAt = this.now().toISOString();
484
- const event: DelegationEvent = {
485
- type: "cancelled",
486
- taskId: task.taskId,
487
- callId: task.callId,
488
- };
489
- task.events.push(event);
490
- await this.audit.markCancelled(task.requestId, {
491
- reason: "delegation_cancelled",
492
- now: this.now,
493
- });
494
- yield event;
495
- }
496
- }
497
- }
498
-
499
- function mapProviderEvent(
500
- taskId: string,
501
- event: import("./providers/adapter.js").ProviderEvent,
502
- ): DelegationEvent | undefined {
503
- switch (event.type) {
504
- case "start":
505
- return {
506
- type: "start",
507
- taskId,
508
- callId: event.callId,
509
- model: event.model,
510
- };
511
- case "text-delta":
512
- return {
513
- type: "text-delta",
514
- taskId,
515
- callId: event.callId,
516
- text: event.text,
517
- };
518
- case "usage":
519
- return {
520
- type: "usage",
521
- taskId,
522
- callId: event.callId,
523
- inputTokens: event.inputTokens,
524
- outputTokens: event.outputTokens,
525
- totalTokens: event.totalTokens,
526
- };
527
- case "done":
528
- return {
529
- type: "done",
530
- taskId,
531
- callId: event.callId,
532
- finishReason: event.finishReason,
533
- };
534
- case "error":
535
- return {
536
- type: "error",
537
- taskId,
538
- callId: event.callId,
539
- message: event.error.message,
540
- code: event.error.code,
541
- };
542
- case "tool-call":
543
- return undefined;
544
- default:
545
- return undefined;
546
- }
547
- }
548
-
549
- function statusOf(task: DelegationTask): DelegationTaskStatus {
550
- return task.status;
551
- }
552
-
553
- function applyRouteOverrideDenial(
554
- request: DelegationRequest,
555
- decision: RouteDecision,
556
- ): RouteDecision {
557
- if (!decision.route) return decision;
558
-
559
- const denials: PolicyDenial[] = [];
560
- if (
561
- request.providerId !== undefined &&
562
- request.providerId !== decision.route.provider
563
- ) {
564
- denials.push({
565
- code: "provider_denied",
566
- target: "provider",
567
- id: request.providerId,
568
- message: `Requested provider ${request.providerId} does not match policy-selected provider ${decision.route.provider}.`,
569
- });
570
- }
571
- if (request.model !== undefined && request.model !== decision.route.model) {
572
- denials.push({
573
- code: "provider_denied",
574
- target: "model",
575
- id: request.model,
576
- message: `Requested model ${request.model} does not match policy-selected model ${decision.route.model}.`,
577
- });
578
- }
579
-
580
- if (denials.length === 0) return decision;
581
-
582
- return {
583
- ...decision,
584
- route: null,
585
- reason: "provider_override_denied",
586
- explanation: denials.map((d) => d.message).join(" "),
587
- policyDenials: [...(decision.policyDenials ?? []), ...denials],
588
- blocked: true,
589
- needsConfiguration: false,
590
- confidence: 1,
591
- auditSummary: `Blocked provider override profile=${request.route.harness.profile} digest=${request.route.task.promptDigest}`,
592
- };
593
- }
594
-
595
- function cloneTask(task: DelegationTask): DelegationTask {
596
- return {
597
- ...task,
598
- events: [...task.events],
599
- decision: task.decision ? { ...task.decision } : undefined,
600
- };
601
- }
602
-
603
- function sleep(ms: number): Promise<void> {
604
- return new Promise((resolve) => setTimeout(resolve, ms));
605
- }