omk-agent-core 0.98.3 → 0.98.4

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 (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/agent-loop.d.ts +1 -31
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +15 -70
  5. package/dist/agent-loop.js.map +1 -1
  6. package/dist/agent.d.ts +7 -0
  7. package/dist/agent.d.ts.map +1 -1
  8. package/dist/agent.js +10 -29
  9. package/dist/agent.js.map +1 -1
  10. package/dist/harness/reverse-skill-text.d.ts +3 -0
  11. package/dist/harness/reverse-skill-text.d.ts.map +1 -0
  12. package/dist/harness/reverse-skill-text.js +20 -0
  13. package/dist/harness/reverse-skill-text.js.map +1 -0
  14. package/dist/harness/reverse-skill-tool-aliases.d.ts +3 -0
  15. package/dist/harness/reverse-skill-tool-aliases.d.ts.map +1 -0
  16. package/dist/harness/reverse-skill-tool-aliases.js +47 -0
  17. package/dist/harness/reverse-skill-tool-aliases.js.map +1 -0
  18. package/dist/harness/reverse-skill-types.d.ts +76 -0
  19. package/dist/harness/reverse-skill-types.d.ts.map +1 -0
  20. package/dist/harness/reverse-skill-types.js +2 -0
  21. package/dist/harness/reverse-skill-types.js.map +1 -0
  22. package/dist/harness/reverse-skill.d.ts +4 -78
  23. package/dist/harness/reverse-skill.d.ts.map +1 -1
  24. package/dist/harness/reverse-skill.js +113 -61
  25. package/dist/harness/reverse-skill.js.map +1 -1
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/pending-message-queue.d.ts +11 -0
  31. package/dist/pending-message-queue.d.ts.map +1 -0
  32. package/dist/pending-message-queue.js +29 -0
  33. package/dist/pending-message-queue.js.map +1 -0
  34. package/dist/provider-input.d.ts +8 -0
  35. package/dist/provider-input.d.ts.map +1 -0
  36. package/dist/provider-input.js +28 -0
  37. package/dist/provider-input.js.map +1 -0
  38. package/dist/provider-payload-contract.d.ts +6 -0
  39. package/dist/provider-payload-contract.d.ts.map +1 -0
  40. package/dist/provider-payload-contract.js +43 -0
  41. package/dist/provider-payload-contract.js.map +1 -0
  42. package/dist/provider-request-types.d.ts +46 -0
  43. package/dist/provider-request-types.d.ts.map +1 -0
  44. package/dist/provider-request-types.js +2 -0
  45. package/dist/provider-request-types.js.map +1 -0
  46. package/dist/provider-request.d.ts +17 -0
  47. package/dist/provider-request.d.ts.map +1 -0
  48. package/dist/provider-request.js +110 -0
  49. package/dist/provider-request.js.map +1 -0
  50. package/dist/run-model-contract.d.ts +11 -0
  51. package/dist/run-model-contract.d.ts.map +1 -0
  52. package/dist/run-model-contract.js +121 -0
  53. package/dist/run-model-contract.js.map +1 -0
  54. package/dist/types.d.ts +4 -1
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/vision-route.d.ts +19 -0
  58. package/dist/vision-route.d.ts.map +1 -0
  59. package/dist/vision-route.js +19 -0
  60. package/dist/vision-route.js.map +1 -0
  61. package/package.json +2 -2
@@ -0,0 +1,46 @@
1
+ import type { SimpleStreamOptions } from "omk-ai";
2
+ type ContractThinkingLevel = "off" | NonNullable<SimpleStreamOptions["reasoning"]>;
3
+ export interface ModelIdentity {
4
+ readonly provider: string;
5
+ readonly id: string;
6
+ }
7
+ /** Logical dispatch policy, not endpoint, credential, or serialized-payload attestation. */
8
+ export interface ModelContract {
9
+ readonly allowedModels: readonly ModelIdentity[];
10
+ readonly allowedProviders: readonly string[];
11
+ readonly allowedAuthOrigins: readonly string[];
12
+ readonly thinking: boolean;
13
+ readonly maxOutputTokens: number;
14
+ readonly thinkingLevel?: ContractThinkingLevel;
15
+ }
16
+ export interface RouteRequest {
17
+ readonly model: ModelIdentity;
18
+ readonly provider: string;
19
+ readonly thinking: boolean;
20
+ readonly maxOutputTokens?: number;
21
+ /** Logical credential resolver identity; omission means request.provider. */
22
+ readonly authOrigin?: string;
23
+ readonly thinkingLevel?: ContractThinkingLevel;
24
+ }
25
+ export type ModelContractViolationCode = "invalid-contract" | "invalid-contract-limit" | "invalid-request" | "invalid-request-limit" | "model-not-allowed" | "provider-not-allowed" | "provider-mismatch" | "auth-origin-not-allowed" | "thinking-not-allowed" | "thinking-level-mismatch" | "output-limit-exceeded";
26
+ export type ProviderRequestEvent = {
27
+ readonly type: "provider_denied";
28
+ readonly requestId: string;
29
+ readonly deniedReason: "contract-violation" | "aborted";
30
+ } | {
31
+ readonly type: "provider_request";
32
+ readonly requestId: string;
33
+ readonly provider: string;
34
+ readonly model: string;
35
+ readonly maxOutputTokens: number;
36
+ /** Tool attachments replaced by explicit text placeholders in this provider view. */
37
+ readonly omittedToolImages?: number;
38
+ readonly boundary: "stream-dispatch";
39
+ } | {
40
+ readonly type: "provider_request_end";
41
+ readonly requestId: string;
42
+ readonly outcome: "completed" | "error" | "aborted";
43
+ readonly boundary: "stream-dispatch";
44
+ };
45
+ export {};
46
+ //# sourceMappingURL=provider-request-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-request-types.d.ts","sourceRoot":"","sources":["../src/provider-request-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAElD,KAAK,qBAAqB,GAAG,KAAK,GAAG,WAAW,CAAC,mBAAmB,CAAC,WAAW,CAAC,CAAC,CAAC;AACnF,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACpB;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,aAAa,EAAE,SAAS,aAAa,EAAE,CAAC;IACjD,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,qBAAqB,CAAC;CAC/C;AAED,MAAM,MAAM,0BAA0B,GACnC,kBAAkB,GAClB,wBAAwB,GACxB,iBAAiB,GACjB,uBAAuB,GACvB,mBAAmB,GACnB,sBAAsB,GACtB,mBAAmB,GACnB,yBAAyB,GACzB,sBAAsB,GACtB,yBAAyB,GACzB,uBAAuB,CAAC;AAE3B,MAAM,MAAM,oBAAoB,GAC7B;IACA,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,oBAAoB,GAAG,SAAS,CAAC;CACvD,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,qFAAqF;IACrF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;CACpC,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;CACpC,CAAC","sourcesContent":["import type { SimpleStreamOptions } from \"omk-ai\";\n\ntype ContractThinkingLevel = \"off\" | NonNullable<SimpleStreamOptions[\"reasoning\"]>;\nexport interface ModelIdentity {\n\treadonly provider: string;\n\treadonly id: string;\n}\n\n/** Logical dispatch policy, not endpoint, credential, or serialized-payload attestation. */\nexport interface ModelContract {\n\treadonly allowedModels: readonly ModelIdentity[];\n\treadonly allowedProviders: readonly string[];\n\treadonly allowedAuthOrigins: readonly string[];\n\treadonly thinking: boolean;\n\treadonly maxOutputTokens: number;\n\treadonly thinkingLevel?: ContractThinkingLevel;\n}\n\nexport interface RouteRequest {\n\treadonly model: ModelIdentity;\n\treadonly provider: string;\n\treadonly thinking: boolean;\n\treadonly maxOutputTokens?: number;\n\t/** Logical credential resolver identity; omission means request.provider. */\n\treadonly authOrigin?: string;\n\treadonly thinkingLevel?: ContractThinkingLevel;\n}\n\nexport type ModelContractViolationCode =\n\t| \"invalid-contract\"\n\t| \"invalid-contract-limit\"\n\t| \"invalid-request\"\n\t| \"invalid-request-limit\"\n\t| \"model-not-allowed\"\n\t| \"provider-not-allowed\"\n\t| \"provider-mismatch\"\n\t| \"auth-origin-not-allowed\"\n\t| \"thinking-not-allowed\"\n\t| \"thinking-level-mismatch\"\n\t| \"output-limit-exceeded\";\n\nexport type ProviderRequestEvent =\n\t| {\n\t\t\treadonly type: \"provider_denied\";\n\t\t\treadonly requestId: string;\n\t\t\treadonly deniedReason: \"contract-violation\" | \"aborted\";\n\t }\n\t| {\n\t\t\treadonly type: \"provider_request\";\n\t\t\treadonly requestId: string;\n\t\t\treadonly provider: string;\n\t\t\treadonly model: string;\n\t\t\treadonly maxOutputTokens: number;\n\t\t\t/** Tool attachments replaced by explicit text placeholders in this provider view. */\n\t\t\treadonly omittedToolImages?: number;\n\t\t\treadonly boundary: \"stream-dispatch\";\n\t }\n\t| {\n\t\t\treadonly type: \"provider_request_end\";\n\t\t\treadonly requestId: string;\n\t\t\treadonly outcome: \"completed\" | \"error\" | \"aborted\";\n\t\t\treadonly boundary: \"stream-dispatch\";\n\t };\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=provider-request-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-request-types.js","sourceRoot":"","sources":["../src/provider-request-types.ts"],"names":[],"mappings":"","sourcesContent":["import type { SimpleStreamOptions } from \"omk-ai\";\n\ntype ContractThinkingLevel = \"off\" | NonNullable<SimpleStreamOptions[\"reasoning\"]>;\nexport interface ModelIdentity {\n\treadonly provider: string;\n\treadonly id: string;\n}\n\n/** Logical dispatch policy, not endpoint, credential, or serialized-payload attestation. */\nexport interface ModelContract {\n\treadonly allowedModels: readonly ModelIdentity[];\n\treadonly allowedProviders: readonly string[];\n\treadonly allowedAuthOrigins: readonly string[];\n\treadonly thinking: boolean;\n\treadonly maxOutputTokens: number;\n\treadonly thinkingLevel?: ContractThinkingLevel;\n}\n\nexport interface RouteRequest {\n\treadonly model: ModelIdentity;\n\treadonly provider: string;\n\treadonly thinking: boolean;\n\treadonly maxOutputTokens?: number;\n\t/** Logical credential resolver identity; omission means request.provider. */\n\treadonly authOrigin?: string;\n\treadonly thinkingLevel?: ContractThinkingLevel;\n}\n\nexport type ModelContractViolationCode =\n\t| \"invalid-contract\"\n\t| \"invalid-contract-limit\"\n\t| \"invalid-request\"\n\t| \"invalid-request-limit\"\n\t| \"model-not-allowed\"\n\t| \"provider-not-allowed\"\n\t| \"provider-mismatch\"\n\t| \"auth-origin-not-allowed\"\n\t| \"thinking-not-allowed\"\n\t| \"thinking-level-mismatch\"\n\t| \"output-limit-exceeded\";\n\nexport type ProviderRequestEvent =\n\t| {\n\t\t\treadonly type: \"provider_denied\";\n\t\t\treadonly requestId: string;\n\t\t\treadonly deniedReason: \"contract-violation\" | \"aborted\";\n\t }\n\t| {\n\t\t\treadonly type: \"provider_request\";\n\t\t\treadonly requestId: string;\n\t\t\treadonly provider: string;\n\t\t\treadonly model: string;\n\t\t\treadonly maxOutputTokens: number;\n\t\t\t/** Tool attachments replaced by explicit text placeholders in this provider view. */\n\t\t\treadonly omittedToolImages?: number;\n\t\t\treadonly boundary: \"stream-dispatch\";\n\t }\n\t| {\n\t\t\treadonly type: \"provider_request_end\";\n\t\t\treadonly requestId: string;\n\t\t\treadonly outcome: \"completed\" | \"error\" | \"aborted\";\n\t\t\treadonly boundary: \"stream-dispatch\";\n\t };\n"]}
@@ -0,0 +1,17 @@
1
+ import { type Api, type AssistantMessage, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions } from "omk-ai";
2
+ import { type ModelContract } from "./run-model-contract.ts";
3
+ import type { AgentEvent, AgentLoopConfig, StreamFn } from "./types.ts";
4
+ export type { ProviderRequestEvent } from "./provider-request-types.ts";
5
+ /** Pin policy before any lifecycle subscriber can alter caller-owned configuration. */
6
+ export declare function pinProviderConfig(config: AgentLoopConfig, emit: (event: AgentEvent) => Promise<void> | void): Promise<AgentLoopConfig>;
7
+ /** Restrict logical dispatch and attach the supported final-payload checks. */
8
+ export declare function applyModelContract(contract: ModelContract, model: Model<Api>, options: SimpleStreamOptions): SimpleStreamOptions;
9
+ interface ProviderRuntime {
10
+ readonly signal?: AbortSignal;
11
+ readonly emit: (event: AgentEvent) => Promise<void> | void;
12
+ readonly streamFn?: StreamFn;
13
+ readonly consume: (stream: AssistantMessageEventStream) => Promise<AssistantMessage>;
14
+ }
15
+ /** Emit only bounded dispatch metadata; never payloads, headers, keys, or raw failures. */
16
+ export declare function requestAssistantResponse(context: Context, config: AgentLoopConfig, runtime: ProviderRuntime): Promise<AssistantMessage>;
17
+ //# sourceMappingURL=provider-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-request.d.ts","sourceRoot":"","sources":["../src/provider-request.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,GAAG,EACR,KAAK,gBAAgB,EACrB,KAAK,2BAA2B,EAChC,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,mBAAmB,EAExB,MAAM,QAAQ,CAAC;AAGhB,OAAO,EAEN,KAAK,aAAa,EAGlB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGxE,YAAY,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,uFAAuF;AACvF,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC/C,OAAO,CAAC,eAAe,CAAC,CAU1B;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,EAAE,mBAAmB,GAC1B,mBAAmB,CAerB;AAED,UAAU,eAAe;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,2BAA2B,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACrF;AAED,2FAA2F;AAC3F,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,eAAe,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAsE3B","sourcesContent":["import {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\ttype Model,\n\ttype SimpleStreamOptions,\n\tstreamSimple,\n} from \"omk-ai\";\nimport { projectToolImagesForModel } from \"./provider-input.ts\";\nimport { createContractPayloadHook } from \"./provider-payload-contract.ts\";\nimport {\n\tassertModelContract,\n\ttype ModelContract,\n\tModelContractViolation,\n\tsnapshotModelContract,\n} from \"./run-model-contract.ts\";\nimport type { AgentEvent, AgentLoopConfig, StreamFn } from \"./types.ts\";\nimport { getVisionRouteModel } from \"./vision-route.ts\";\n\nexport type { ProviderRequestEvent } from \"./provider-request-types.ts\";\n\n/** Pin policy before any lifecycle subscriber can alter caller-owned configuration. */\nexport async function pinProviderConfig(\n\tconfig: AgentLoopConfig,\n\temit: (event: AgentEvent) => Promise<void> | void,\n): Promise<AgentLoopConfig> {\n\tif (config.modelContract === undefined) return config;\n\ttry {\n\t\treturn { ...config, modelContract: snapshotModelContract(config.modelContract) };\n\t} catch (error) {\n\t\tif (error instanceof ModelContractViolation) {\n\t\t\tawait emit({ type: \"provider_denied\", requestId: crypto.randomUUID(), deniedReason: \"contract-violation\" });\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/** Restrict logical dispatch and attach the supported final-payload checks. */\nexport function applyModelContract(\n\tcontract: ModelContract,\n\tmodel: Model<Api>,\n\toptions: SimpleStreamOptions,\n): SimpleStreamOptions {\n\tconst maxTokens = options.maxTokens ?? Math.min(model.maxTokens, contract.maxOutputTokens);\n\tassertModelContract(contract, {\n\t\tmodel,\n\t\tprovider: model.provider,\n\t\tauthOrigin: model.provider,\n\t\tthinking: options.reasoning !== undefined,\n\t\tthinkingLevel: options.reasoning ?? \"off\",\n\t\tmaxOutputTokens: maxTokens,\n\t});\n\treturn {\n\t\t...options,\n\t\tmaxTokens,\n\t\tonPayload: createContractPayloadHook(model, maxTokens, options.onPayload),\n\t};\n}\n\ninterface ProviderRuntime {\n\treadonly signal?: AbortSignal;\n\treadonly emit: (event: AgentEvent) => Promise<void> | void;\n\treadonly streamFn?: StreamFn;\n\treadonly consume: (stream: AssistantMessageEventStream) => Promise<AssistantMessage>;\n}\n\n/** Emit only bounded dispatch metadata; never payloads, headers, keys, or raw failures. */\nexport async function requestAssistantResponse(\n\tcontext: Context,\n\tconfig: AgentLoopConfig,\n\truntime: ProviderRuntime,\n): Promise<AssistantMessage> {\n\tconst { signal, emit, consume } = runtime;\n\tconst needsVisionRoute = context.messages.some(\n\t\t(message) =>\n\t\t\t(!config.modelContract || message.role !== \"toolResult\") &&\n\t\t\tArray.isArray(message.content) &&\n\t\t\tmessage.content.some((part) => part.type === \"image\"),\n\t);\n\tconst model =\n\t\tneedsVisionRoute && !(config.model.input ?? []).includes(\"image\")\n\t\t\t? getVisionRouteModel(config.model)\n\t\t\t: config.model;\n\tconst projection = config.modelContract\n\t\t? projectToolImagesForModel(context, model)\n\t\t: { context, omittedToolImages: 0 };\n\tconst changedProvider = model.provider !== config.model.provider;\n\tconst requestId = crypto.randomUUID();\n\tconst { modelContract, ...configured } = config;\n\tlet options: SimpleStreamOptions = configured;\n\ttry {\n\t\tif (signal?.aborted) {\n\t\t\tif (modelContract) await emit({ type: \"provider_denied\", requestId, deniedReason: \"aborted\" });\n\t\t\tsignal.throwIfAborted();\n\t\t}\n\t\tif (modelContract) options = applyModelContract(modelContract, model, configured);\n\t} catch (error) {\n\t\tif (error instanceof ModelContractViolation) {\n\t\t\tawait emit({ type: \"provider_denied\", requestId, deniedReason: \"contract-violation\" });\n\t\t}\n\t\tthrow error;\n\t}\n\tconst key = await config.getApiKey?.(model.provider);\n\tif (signal?.aborted) {\n\t\tif (modelContract) await emit({ type: \"provider_denied\", requestId, deniedReason: \"aborted\" });\n\t\tsignal.throwIfAborted();\n\t}\n\tconst requestOptions: SimpleStreamOptions = {\n\t\t...options,\n\t\tapiKey: key || (changedProvider ? undefined : options.apiKey),\n\t\theaders: changedProvider ? undefined : options.headers,\n\t\tsignal,\n\t};\n\tlet outcome: \"completed\" | \"error\" | \"aborted\" = \"error\";\n\ttry {\n\t\tif (modelContract) {\n\t\t\tawait emit({\n\t\t\t\ttype: \"provider_request\",\n\t\t\t\trequestId,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tmaxOutputTokens: requestOptions.maxTokens ?? modelContract.maxOutputTokens,\n\t\t\t\t...(projection.omittedToolImages > 0 ? { omittedToolImages: projection.omittedToolImages } : {}),\n\t\t\t\tboundary: \"stream-dispatch\",\n\t\t\t});\n\t\t}\n\t\tsignal?.throwIfAborted();\n\t\tconst response = await (runtime.streamFn ?? streamSimple)(model, projection.context, requestOptions);\n\t\tconst message = await consume(response);\n\t\toutcome = message.stopReason === \"aborted\" ? \"aborted\" : message.stopReason === \"error\" ? \"error\" : \"completed\";\n\t\treturn message;\n\t} finally {\n\t\tif (modelContract) {\n\t\t\tawait emit({\n\t\t\t\ttype: \"provider_request_end\",\n\t\t\t\trequestId,\n\t\t\t\toutcome: signal?.aborted ? \"aborted\" : outcome,\n\t\t\t\tboundary: \"stream-dispatch\",\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,110 @@
1
+ import { streamSimple, } from "omk-ai";
2
+ import { projectToolImagesForModel } from "./provider-input.js";
3
+ import { createContractPayloadHook } from "./provider-payload-contract.js";
4
+ import { assertModelContract, ModelContractViolation, snapshotModelContract, } from "./run-model-contract.js";
5
+ import { getVisionRouteModel } from "./vision-route.js";
6
+ /** Pin policy before any lifecycle subscriber can alter caller-owned configuration. */
7
+ export async function pinProviderConfig(config, emit) {
8
+ if (config.modelContract === undefined)
9
+ return config;
10
+ try {
11
+ return { ...config, modelContract: snapshotModelContract(config.modelContract) };
12
+ }
13
+ catch (error) {
14
+ if (error instanceof ModelContractViolation) {
15
+ await emit({ type: "provider_denied", requestId: crypto.randomUUID(), deniedReason: "contract-violation" });
16
+ }
17
+ throw error;
18
+ }
19
+ }
20
+ /** Restrict logical dispatch and attach the supported final-payload checks. */
21
+ export function applyModelContract(contract, model, options) {
22
+ const maxTokens = options.maxTokens ?? Math.min(model.maxTokens, contract.maxOutputTokens);
23
+ assertModelContract(contract, {
24
+ model,
25
+ provider: model.provider,
26
+ authOrigin: model.provider,
27
+ thinking: options.reasoning !== undefined,
28
+ thinkingLevel: options.reasoning ?? "off",
29
+ maxOutputTokens: maxTokens,
30
+ });
31
+ return {
32
+ ...options,
33
+ maxTokens,
34
+ onPayload: createContractPayloadHook(model, maxTokens, options.onPayload),
35
+ };
36
+ }
37
+ /** Emit only bounded dispatch metadata; never payloads, headers, keys, or raw failures. */
38
+ export async function requestAssistantResponse(context, config, runtime) {
39
+ const { signal, emit, consume } = runtime;
40
+ const needsVisionRoute = context.messages.some((message) => (!config.modelContract || message.role !== "toolResult") &&
41
+ Array.isArray(message.content) &&
42
+ message.content.some((part) => part.type === "image"));
43
+ const model = needsVisionRoute && !(config.model.input ?? []).includes("image")
44
+ ? getVisionRouteModel(config.model)
45
+ : config.model;
46
+ const projection = config.modelContract
47
+ ? projectToolImagesForModel(context, model)
48
+ : { context, omittedToolImages: 0 };
49
+ const changedProvider = model.provider !== config.model.provider;
50
+ const requestId = crypto.randomUUID();
51
+ const { modelContract, ...configured } = config;
52
+ let options = configured;
53
+ try {
54
+ if (signal?.aborted) {
55
+ if (modelContract)
56
+ await emit({ type: "provider_denied", requestId, deniedReason: "aborted" });
57
+ signal.throwIfAborted();
58
+ }
59
+ if (modelContract)
60
+ options = applyModelContract(modelContract, model, configured);
61
+ }
62
+ catch (error) {
63
+ if (error instanceof ModelContractViolation) {
64
+ await emit({ type: "provider_denied", requestId, deniedReason: "contract-violation" });
65
+ }
66
+ throw error;
67
+ }
68
+ const key = await config.getApiKey?.(model.provider);
69
+ if (signal?.aborted) {
70
+ if (modelContract)
71
+ await emit({ type: "provider_denied", requestId, deniedReason: "aborted" });
72
+ signal.throwIfAborted();
73
+ }
74
+ const requestOptions = {
75
+ ...options,
76
+ apiKey: key || (changedProvider ? undefined : options.apiKey),
77
+ headers: changedProvider ? undefined : options.headers,
78
+ signal,
79
+ };
80
+ let outcome = "error";
81
+ try {
82
+ if (modelContract) {
83
+ await emit({
84
+ type: "provider_request",
85
+ requestId,
86
+ provider: model.provider,
87
+ model: model.id,
88
+ maxOutputTokens: requestOptions.maxTokens ?? modelContract.maxOutputTokens,
89
+ ...(projection.omittedToolImages > 0 ? { omittedToolImages: projection.omittedToolImages } : {}),
90
+ boundary: "stream-dispatch",
91
+ });
92
+ }
93
+ signal?.throwIfAborted();
94
+ const response = await (runtime.streamFn ?? streamSimple)(model, projection.context, requestOptions);
95
+ const message = await consume(response);
96
+ outcome = message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
97
+ return message;
98
+ }
99
+ finally {
100
+ if (modelContract) {
101
+ await emit({
102
+ type: "provider_request_end",
103
+ requestId,
104
+ outcome: signal?.aborted ? "aborted" : outcome,
105
+ boundary: "stream-dispatch",
106
+ });
107
+ }
108
+ }
109
+ }
110
+ //# sourceMappingURL=provider-request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-request.js","sourceRoot":"","sources":["../src/provider-request.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype AssistantMessageEventStream,\n\ttype Context,\n\ttype Model,\n\ttype SimpleStreamOptions,\n\tstreamSimple,\n} from \"omk-ai\";\nimport { projectToolImagesForModel } from \"./provider-input.ts\";\nimport { createContractPayloadHook } from \"./provider-payload-contract.ts\";\nimport {\n\tassertModelContract,\n\ttype ModelContract,\n\tModelContractViolation,\n\tsnapshotModelContract,\n} from \"./run-model-contract.ts\";\nimport type { AgentEvent, AgentLoopConfig, StreamFn } from \"./types.ts\";\nimport { getVisionRouteModel } from \"./vision-route.ts\";\n\nexport type { ProviderRequestEvent } from \"./provider-request-types.ts\";\n\n/** Pin policy before any lifecycle subscriber can alter caller-owned configuration. */\nexport async function pinProviderConfig(\n\tconfig: AgentLoopConfig,\n\temit: (event: AgentEvent) => Promise<void> | void,\n): Promise<AgentLoopConfig> {\n\tif (config.modelContract === undefined) return config;\n\ttry {\n\t\treturn { ...config, modelContract: snapshotModelContract(config.modelContract) };\n\t} catch (error) {\n\t\tif (error instanceof ModelContractViolation) {\n\t\t\tawait emit({ type: \"provider_denied\", requestId: crypto.randomUUID(), deniedReason: \"contract-violation\" });\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/** Restrict logical dispatch and attach the supported final-payload checks. */\nexport function applyModelContract(\n\tcontract: ModelContract,\n\tmodel: Model<Api>,\n\toptions: SimpleStreamOptions,\n): SimpleStreamOptions {\n\tconst maxTokens = options.maxTokens ?? Math.min(model.maxTokens, contract.maxOutputTokens);\n\tassertModelContract(contract, {\n\t\tmodel,\n\t\tprovider: model.provider,\n\t\tauthOrigin: model.provider,\n\t\tthinking: options.reasoning !== undefined,\n\t\tthinkingLevel: options.reasoning ?? \"off\",\n\t\tmaxOutputTokens: maxTokens,\n\t});\n\treturn {\n\t\t...options,\n\t\tmaxTokens,\n\t\tonPayload: createContractPayloadHook(model, maxTokens, options.onPayload),\n\t};\n}\n\ninterface ProviderRuntime {\n\treadonly signal?: AbortSignal;\n\treadonly emit: (event: AgentEvent) => Promise<void> | void;\n\treadonly streamFn?: StreamFn;\n\treadonly consume: (stream: AssistantMessageEventStream) => Promise<AssistantMessage>;\n}\n\n/** Emit only bounded dispatch metadata; never payloads, headers, keys, or raw failures. */\nexport async function requestAssistantResponse(\n\tcontext: Context,\n\tconfig: AgentLoopConfig,\n\truntime: ProviderRuntime,\n): Promise<AssistantMessage> {\n\tconst { signal, emit, consume } = runtime;\n\tconst needsVisionRoute = context.messages.some(\n\t\t(message) =>\n\t\t\t(!config.modelContract || message.role !== \"toolResult\") &&\n\t\t\tArray.isArray(message.content) &&\n\t\t\tmessage.content.some((part) => part.type === \"image\"),\n\t);\n\tconst model =\n\t\tneedsVisionRoute && !(config.model.input ?? []).includes(\"image\")\n\t\t\t? getVisionRouteModel(config.model)\n\t\t\t: config.model;\n\tconst projection = config.modelContract\n\t\t? projectToolImagesForModel(context, model)\n\t\t: { context, omittedToolImages: 0 };\n\tconst changedProvider = model.provider !== config.model.provider;\n\tconst requestId = crypto.randomUUID();\n\tconst { modelContract, ...configured } = config;\n\tlet options: SimpleStreamOptions = configured;\n\ttry {\n\t\tif (signal?.aborted) {\n\t\t\tif (modelContract) await emit({ type: \"provider_denied\", requestId, deniedReason: \"aborted\" });\n\t\t\tsignal.throwIfAborted();\n\t\t}\n\t\tif (modelContract) options = applyModelContract(modelContract, model, configured);\n\t} catch (error) {\n\t\tif (error instanceof ModelContractViolation) {\n\t\t\tawait emit({ type: \"provider_denied\", requestId, deniedReason: \"contract-violation\" });\n\t\t}\n\t\tthrow error;\n\t}\n\tconst key = await config.getApiKey?.(model.provider);\n\tif (signal?.aborted) {\n\t\tif (modelContract) await emit({ type: \"provider_denied\", requestId, deniedReason: \"aborted\" });\n\t\tsignal.throwIfAborted();\n\t}\n\tconst requestOptions: SimpleStreamOptions = {\n\t\t...options,\n\t\tapiKey: key || (changedProvider ? undefined : options.apiKey),\n\t\theaders: changedProvider ? undefined : options.headers,\n\t\tsignal,\n\t};\n\tlet outcome: \"completed\" | \"error\" | \"aborted\" = \"error\";\n\ttry {\n\t\tif (modelContract) {\n\t\t\tawait emit({\n\t\t\t\ttype: \"provider_request\",\n\t\t\t\trequestId,\n\t\t\t\tprovider: model.provider,\n\t\t\t\tmodel: model.id,\n\t\t\t\tmaxOutputTokens: requestOptions.maxTokens ?? modelContract.maxOutputTokens,\n\t\t\t\t...(projection.omittedToolImages > 0 ? { omittedToolImages: projection.omittedToolImages } : {}),\n\t\t\t\tboundary: \"stream-dispatch\",\n\t\t\t});\n\t\t}\n\t\tsignal?.throwIfAborted();\n\t\tconst response = await (runtime.streamFn ?? streamSimple)(model, projection.context, requestOptions);\n\t\tconst message = await consume(response);\n\t\toutcome = message.stopReason === \"aborted\" ? \"aborted\" : message.stopReason === \"error\" ? \"error\" : \"completed\";\n\t\treturn message;\n\t} finally {\n\t\tif (modelContract) {\n\t\t\tawait emit({\n\t\t\t\ttype: \"provider_request_end\",\n\t\t\t\trequestId,\n\t\t\t\toutcome: signal?.aborted ? \"aborted\" : outcome,\n\t\t\t\tboundary: \"stream-dispatch\",\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { ModelContract, ModelContractViolationCode, RouteRequest } from "./provider-request-types.ts";
2
+ export type { ModelContract, ModelContractViolationCode, RouteRequest } from "./provider-request-types.ts";
3
+ export declare class ModelContractViolation extends Error {
4
+ readonly code: ModelContractViolationCode;
5
+ constructor(code: ModelContractViolationCode);
6
+ }
7
+ /** Compatibility validator: an omitted limit is not evidence of an effective cap. */
8
+ export declare function assertModelContract(contract: ModelContract, request: RouteRequest): void;
9
+ /** Copy only policy fields, never full model metadata or credential-bearing headers. */
10
+ export declare function snapshotModelContract(contract: unknown): ModelContract;
11
+ //# sourceMappingURL=run-model-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-model-contract.d.ts","sourceRoot":"","sources":["../src/run-model-contract.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EACb,0BAA0B,EAE1B,YAAY,EACZ,MAAM,6BAA6B,CAAC;AAErC,YAAY,EAAE,aAAa,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAiB3G,qBAAa,sBAAuB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;IAE1C,YAAY,IAAI,EAAE,0BAA0B,EAK3C;CACD;AA4DD,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,GAAG,IAAI,CAmCxF;AAED,wFAAwF;AACxF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,aAAa,CAUtE","sourcesContent":["import { createImmutableSnapshot } from \"./plain-data.ts\";\nimport type {\n\tModelContract,\n\tModelContractViolationCode,\n\tModelIdentity,\n\tRouteRequest,\n} from \"./provider-request-types.ts\";\n\nexport type { ModelContract, ModelContractViolationCode, RouteRequest } from \"./provider-request-types.ts\";\n\nconst MESSAGES: Record<ModelContractViolationCode, string> = {\n\t\"invalid-contract\": \"Invalid model contract shape\",\n\t\"invalid-contract-limit\": \"contract maxOutputTokens must be a positive safe integer\",\n\t\"invalid-request\": \"Invalid model contract request shape\",\n\t\"invalid-request-limit\": \"request maxOutputTokens must be a positive safe integer\",\n\t\"model-not-allowed\": \"Model is not allowed by the model contract\",\n\t\"provider-not-allowed\": \"Provider is not allowed by the model contract\",\n\t\"provider-mismatch\": \"Request provider does not match model provider\",\n\t\"auth-origin-not-allowed\": \"Credential resolver origin is not allowed by the model contract\",\n\t\"thinking-not-allowed\": \"Thinking is not allowed by the model contract\",\n\t\"thinking-level-mismatch\": \"Thinking level does not match the model contract\",\n\t\"output-limit-exceeded\": \"Request output limit exceeds the model contract\",\n};\nconst LEVELS: readonly string[] = [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\", \"ultra\"];\n\nexport class ModelContractViolation extends Error {\n\treadonly code: ModelContractViolationCode;\n\n\tconstructor(code: ModelContractViolationCode) {\n\t\tconst safeCode = typeof code === \"string\" && Object.hasOwn(MESSAGES, code) ? code : \"invalid-contract\";\n\t\tsuper(MESSAGES[safeCode]);\n\t\tthis.name = \"ModelContractViolation\";\n\t\tthis.code = safeCode;\n\t}\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIdentifier(value: unknown): value is string {\n\treturn (\n\t\ttypeof value === \"string\" &&\n\t\tvalue.length > 0 &&\n\t\tvalue.length <= 256 &&\n\t\tvalue.trim() === value &&\n\t\t!/[\\u0000-\\u001f\\u007f]/u.test(value)\n\t);\n}\n\nfunction isIdentity(value: unknown): value is ModelIdentity {\n\treturn isRecord(value) && isIdentifier(value.provider) && isIdentifier(value.id);\n}\n\nfunction isBoundedList<T>(value: unknown, guard: (entry: unknown) => entry is T): value is readonly T[] {\n\tif (!Array.isArray(value) || value.length < 1 || value.length > 64) return false;\n\t// Array.every skips holes; a sparse allowlist must not silently pass validation.\n\tfor (const entry of value) if (!guard(entry)) return false;\n\treturn true;\n}\n\nfunction validateContract(contract: unknown): asserts contract is ModelContract {\n\tif (!isRecord(contract)) throw new ModelContractViolation(\"invalid-contract\");\n\tif (\n\t\ttypeof contract.maxOutputTokens !== \"number\" ||\n\t\t!Number.isSafeInteger(contract.maxOutputTokens) ||\n\t\tcontract.maxOutputTokens <= 0\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-contract-limit\");\n\t}\n\tif (\n\t\t!isBoundedList(contract.allowedModels, isIdentity) ||\n\t\t!isBoundedList(contract.allowedProviders, isIdentifier) ||\n\t\t!isBoundedList(contract.allowedAuthOrigins, isIdentifier) ||\n\t\ttypeof contract.thinking !== \"boolean\" ||\n\t\tObject.keys(contract).some(\n\t\t\t(key) =>\n\t\t\t\t![\n\t\t\t\t\t\"allowedModels\",\n\t\t\t\t\t\"allowedProviders\",\n\t\t\t\t\t\"allowedAuthOrigins\",\n\t\t\t\t\t\"thinking\",\n\t\t\t\t\t\"maxOutputTokens\",\n\t\t\t\t\t\"thinkingLevel\",\n\t\t\t\t].includes(key),\n\t\t) ||\n\t\t(contract.thinkingLevel !== undefined &&\n\t\t\t(typeof contract.thinkingLevel !== \"string\" || !LEVELS.includes(contract.thinkingLevel))) ||\n\t\t(contract.thinkingLevel !== undefined && contract.thinkingLevel !== \"off\" && !contract.thinking)\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-contract\");\n\t}\n}\n\n/** Compatibility validator: an omitted limit is not evidence of an effective cap. */\nexport function assertModelContract(contract: ModelContract, request: RouteRequest): void {\n\tvalidateContract(contract);\n\tif (\n\t\t!isRecord(request) ||\n\t\t!isIdentity(request.model) ||\n\t\t!isIdentifier(request.provider) ||\n\t\ttypeof request.thinking !== \"boolean\" ||\n\t\t(request.authOrigin !== undefined && !isIdentifier(request.authOrigin)) ||\n\t\t(request.thinkingLevel !== undefined &&\n\t\t\t(!LEVELS.includes(request.thinkingLevel) || (request.thinkingLevel !== \"off\") !== request.thinking))\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-request\");\n\t}\n\tif (\n\t\trequest.maxOutputTokens !== undefined &&\n\t\t(!Number.isSafeInteger(request.maxOutputTokens) || request.maxOutputTokens <= 0)\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-request-limit\");\n\t}\n\tif (request.provider !== request.model.provider) throw new ModelContractViolation(\"provider-mismatch\");\n\tif (!contract.allowedProviders.includes(request.provider)) throw new ModelContractViolation(\"provider-not-allowed\");\n\tif (!contract.allowedModels.some((entry) => entry.provider === request.provider && entry.id === request.model.id)) {\n\t\tthrow new ModelContractViolation(\"model-not-allowed\");\n\t}\n\tif (!contract.allowedAuthOrigins.includes(request.authOrigin ?? request.provider)) {\n\t\tthrow new ModelContractViolation(\"auth-origin-not-allowed\");\n\t}\n\tif (request.thinking && !contract.thinking) throw new ModelContractViolation(\"thinking-not-allowed\");\n\tconst effectiveLevel = request.thinkingLevel ?? \"off\";\n\tif (contract.thinkingLevel !== undefined && contract.thinkingLevel !== effectiveLevel) {\n\t\tthrow new ModelContractViolation(\"thinking-level-mismatch\");\n\t}\n\tif (request.maxOutputTokens !== undefined && request.maxOutputTokens > contract.maxOutputTokens) {\n\t\tthrow new ModelContractViolation(\"output-limit-exceeded\");\n\t}\n}\n\n/** Copy only policy fields, never full model metadata or credential-bearing headers. */\nexport function snapshotModelContract(contract: unknown): ModelContract {\n\tvalidateContract(contract);\n\treturn createImmutableSnapshot({\n\t\tallowedModels: contract.allowedModels.map(({ provider, id }) => ({ provider, id })),\n\t\tallowedProviders: [...contract.allowedProviders],\n\t\tallowedAuthOrigins: [...contract.allowedAuthOrigins],\n\t\tthinking: contract.thinking,\n\t\tmaxOutputTokens: contract.maxOutputTokens,\n\t\t...(contract.thinkingLevel === undefined ? {} : { thinkingLevel: contract.thinkingLevel }),\n\t});\n}\n"]}
@@ -0,0 +1,121 @@
1
+ import { createImmutableSnapshot } from "./plain-data.js";
2
+ const MESSAGES = {
3
+ "invalid-contract": "Invalid model contract shape",
4
+ "invalid-contract-limit": "contract maxOutputTokens must be a positive safe integer",
5
+ "invalid-request": "Invalid model contract request shape",
6
+ "invalid-request-limit": "request maxOutputTokens must be a positive safe integer",
7
+ "model-not-allowed": "Model is not allowed by the model contract",
8
+ "provider-not-allowed": "Provider is not allowed by the model contract",
9
+ "provider-mismatch": "Request provider does not match model provider",
10
+ "auth-origin-not-allowed": "Credential resolver origin is not allowed by the model contract",
11
+ "thinking-not-allowed": "Thinking is not allowed by the model contract",
12
+ "thinking-level-mismatch": "Thinking level does not match the model contract",
13
+ "output-limit-exceeded": "Request output limit exceeds the model contract",
14
+ };
15
+ const LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
16
+ export class ModelContractViolation extends Error {
17
+ code;
18
+ constructor(code) {
19
+ const safeCode = typeof code === "string" && Object.hasOwn(MESSAGES, code) ? code : "invalid-contract";
20
+ super(MESSAGES[safeCode]);
21
+ this.name = "ModelContractViolation";
22
+ this.code = safeCode;
23
+ }
24
+ }
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ function isIdentifier(value) {
29
+ return (typeof value === "string" &&
30
+ value.length > 0 &&
31
+ value.length <= 256 &&
32
+ value.trim() === value &&
33
+ !/[\u0000-\u001f\u007f]/u.test(value));
34
+ }
35
+ function isIdentity(value) {
36
+ return isRecord(value) && isIdentifier(value.provider) && isIdentifier(value.id);
37
+ }
38
+ function isBoundedList(value, guard) {
39
+ if (!Array.isArray(value) || value.length < 1 || value.length > 64)
40
+ return false;
41
+ // Array.every skips holes; a sparse allowlist must not silently pass validation.
42
+ for (const entry of value)
43
+ if (!guard(entry))
44
+ return false;
45
+ return true;
46
+ }
47
+ function validateContract(contract) {
48
+ if (!isRecord(contract))
49
+ throw new ModelContractViolation("invalid-contract");
50
+ if (typeof contract.maxOutputTokens !== "number" ||
51
+ !Number.isSafeInteger(contract.maxOutputTokens) ||
52
+ contract.maxOutputTokens <= 0) {
53
+ throw new ModelContractViolation("invalid-contract-limit");
54
+ }
55
+ if (!isBoundedList(contract.allowedModels, isIdentity) ||
56
+ !isBoundedList(contract.allowedProviders, isIdentifier) ||
57
+ !isBoundedList(contract.allowedAuthOrigins, isIdentifier) ||
58
+ typeof contract.thinking !== "boolean" ||
59
+ Object.keys(contract).some((key) => ![
60
+ "allowedModels",
61
+ "allowedProviders",
62
+ "allowedAuthOrigins",
63
+ "thinking",
64
+ "maxOutputTokens",
65
+ "thinkingLevel",
66
+ ].includes(key)) ||
67
+ (contract.thinkingLevel !== undefined &&
68
+ (typeof contract.thinkingLevel !== "string" || !LEVELS.includes(contract.thinkingLevel))) ||
69
+ (contract.thinkingLevel !== undefined && contract.thinkingLevel !== "off" && !contract.thinking)) {
70
+ throw new ModelContractViolation("invalid-contract");
71
+ }
72
+ }
73
+ /** Compatibility validator: an omitted limit is not evidence of an effective cap. */
74
+ export function assertModelContract(contract, request) {
75
+ validateContract(contract);
76
+ if (!isRecord(request) ||
77
+ !isIdentity(request.model) ||
78
+ !isIdentifier(request.provider) ||
79
+ typeof request.thinking !== "boolean" ||
80
+ (request.authOrigin !== undefined && !isIdentifier(request.authOrigin)) ||
81
+ (request.thinkingLevel !== undefined &&
82
+ (!LEVELS.includes(request.thinkingLevel) || (request.thinkingLevel !== "off") !== request.thinking))) {
83
+ throw new ModelContractViolation("invalid-request");
84
+ }
85
+ if (request.maxOutputTokens !== undefined &&
86
+ (!Number.isSafeInteger(request.maxOutputTokens) || request.maxOutputTokens <= 0)) {
87
+ throw new ModelContractViolation("invalid-request-limit");
88
+ }
89
+ if (request.provider !== request.model.provider)
90
+ throw new ModelContractViolation("provider-mismatch");
91
+ if (!contract.allowedProviders.includes(request.provider))
92
+ throw new ModelContractViolation("provider-not-allowed");
93
+ if (!contract.allowedModels.some((entry) => entry.provider === request.provider && entry.id === request.model.id)) {
94
+ throw new ModelContractViolation("model-not-allowed");
95
+ }
96
+ if (!contract.allowedAuthOrigins.includes(request.authOrigin ?? request.provider)) {
97
+ throw new ModelContractViolation("auth-origin-not-allowed");
98
+ }
99
+ if (request.thinking && !contract.thinking)
100
+ throw new ModelContractViolation("thinking-not-allowed");
101
+ const effectiveLevel = request.thinkingLevel ?? "off";
102
+ if (contract.thinkingLevel !== undefined && contract.thinkingLevel !== effectiveLevel) {
103
+ throw new ModelContractViolation("thinking-level-mismatch");
104
+ }
105
+ if (request.maxOutputTokens !== undefined && request.maxOutputTokens > contract.maxOutputTokens) {
106
+ throw new ModelContractViolation("output-limit-exceeded");
107
+ }
108
+ }
109
+ /** Copy only policy fields, never full model metadata or credential-bearing headers. */
110
+ export function snapshotModelContract(contract) {
111
+ validateContract(contract);
112
+ return createImmutableSnapshot({
113
+ allowedModels: contract.allowedModels.map(({ provider, id }) => ({ provider, id })),
114
+ allowedProviders: [...contract.allowedProviders],
115
+ allowedAuthOrigins: [...contract.allowedAuthOrigins],
116
+ thinking: contract.thinking,
117
+ maxOutputTokens: contract.maxOutputTokens,
118
+ ...(contract.thinkingLevel === undefined ? {} : { thinkingLevel: contract.thinkingLevel }),
119
+ });
120
+ }
121
+ //# sourceMappingURL=run-model-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-model-contract.js","sourceRoot":"","sources":["../src/run-model-contract.ts"],"names":[],"mappings":"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","sourcesContent":["import { createImmutableSnapshot } from \"./plain-data.ts\";\nimport type {\n\tModelContract,\n\tModelContractViolationCode,\n\tModelIdentity,\n\tRouteRequest,\n} from \"./provider-request-types.ts\";\n\nexport type { ModelContract, ModelContractViolationCode, RouteRequest } from \"./provider-request-types.ts\";\n\nconst MESSAGES: Record<ModelContractViolationCode, string> = {\n\t\"invalid-contract\": \"Invalid model contract shape\",\n\t\"invalid-contract-limit\": \"contract maxOutputTokens must be a positive safe integer\",\n\t\"invalid-request\": \"Invalid model contract request shape\",\n\t\"invalid-request-limit\": \"request maxOutputTokens must be a positive safe integer\",\n\t\"model-not-allowed\": \"Model is not allowed by the model contract\",\n\t\"provider-not-allowed\": \"Provider is not allowed by the model contract\",\n\t\"provider-mismatch\": \"Request provider does not match model provider\",\n\t\"auth-origin-not-allowed\": \"Credential resolver origin is not allowed by the model contract\",\n\t\"thinking-not-allowed\": \"Thinking is not allowed by the model contract\",\n\t\"thinking-level-mismatch\": \"Thinking level does not match the model contract\",\n\t\"output-limit-exceeded\": \"Request output limit exceeds the model contract\",\n};\nconst LEVELS: readonly string[] = [\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\", \"ultra\"];\n\nexport class ModelContractViolation extends Error {\n\treadonly code: ModelContractViolationCode;\n\n\tconstructor(code: ModelContractViolationCode) {\n\t\tconst safeCode = typeof code === \"string\" && Object.hasOwn(MESSAGES, code) ? code : \"invalid-contract\";\n\t\tsuper(MESSAGES[safeCode]);\n\t\tthis.name = \"ModelContractViolation\";\n\t\tthis.code = safeCode;\n\t}\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isIdentifier(value: unknown): value is string {\n\treturn (\n\t\ttypeof value === \"string\" &&\n\t\tvalue.length > 0 &&\n\t\tvalue.length <= 256 &&\n\t\tvalue.trim() === value &&\n\t\t!/[\\u0000-\\u001f\\u007f]/u.test(value)\n\t);\n}\n\nfunction isIdentity(value: unknown): value is ModelIdentity {\n\treturn isRecord(value) && isIdentifier(value.provider) && isIdentifier(value.id);\n}\n\nfunction isBoundedList<T>(value: unknown, guard: (entry: unknown) => entry is T): value is readonly T[] {\n\tif (!Array.isArray(value) || value.length < 1 || value.length > 64) return false;\n\t// Array.every skips holes; a sparse allowlist must not silently pass validation.\n\tfor (const entry of value) if (!guard(entry)) return false;\n\treturn true;\n}\n\nfunction validateContract(contract: unknown): asserts contract is ModelContract {\n\tif (!isRecord(contract)) throw new ModelContractViolation(\"invalid-contract\");\n\tif (\n\t\ttypeof contract.maxOutputTokens !== \"number\" ||\n\t\t!Number.isSafeInteger(contract.maxOutputTokens) ||\n\t\tcontract.maxOutputTokens <= 0\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-contract-limit\");\n\t}\n\tif (\n\t\t!isBoundedList(contract.allowedModels, isIdentity) ||\n\t\t!isBoundedList(contract.allowedProviders, isIdentifier) ||\n\t\t!isBoundedList(contract.allowedAuthOrigins, isIdentifier) ||\n\t\ttypeof contract.thinking !== \"boolean\" ||\n\t\tObject.keys(contract).some(\n\t\t\t(key) =>\n\t\t\t\t![\n\t\t\t\t\t\"allowedModels\",\n\t\t\t\t\t\"allowedProviders\",\n\t\t\t\t\t\"allowedAuthOrigins\",\n\t\t\t\t\t\"thinking\",\n\t\t\t\t\t\"maxOutputTokens\",\n\t\t\t\t\t\"thinkingLevel\",\n\t\t\t\t].includes(key),\n\t\t) ||\n\t\t(contract.thinkingLevel !== undefined &&\n\t\t\t(typeof contract.thinkingLevel !== \"string\" || !LEVELS.includes(contract.thinkingLevel))) ||\n\t\t(contract.thinkingLevel !== undefined && contract.thinkingLevel !== \"off\" && !contract.thinking)\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-contract\");\n\t}\n}\n\n/** Compatibility validator: an omitted limit is not evidence of an effective cap. */\nexport function assertModelContract(contract: ModelContract, request: RouteRequest): void {\n\tvalidateContract(contract);\n\tif (\n\t\t!isRecord(request) ||\n\t\t!isIdentity(request.model) ||\n\t\t!isIdentifier(request.provider) ||\n\t\ttypeof request.thinking !== \"boolean\" ||\n\t\t(request.authOrigin !== undefined && !isIdentifier(request.authOrigin)) ||\n\t\t(request.thinkingLevel !== undefined &&\n\t\t\t(!LEVELS.includes(request.thinkingLevel) || (request.thinkingLevel !== \"off\") !== request.thinking))\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-request\");\n\t}\n\tif (\n\t\trequest.maxOutputTokens !== undefined &&\n\t\t(!Number.isSafeInteger(request.maxOutputTokens) || request.maxOutputTokens <= 0)\n\t) {\n\t\tthrow new ModelContractViolation(\"invalid-request-limit\");\n\t}\n\tif (request.provider !== request.model.provider) throw new ModelContractViolation(\"provider-mismatch\");\n\tif (!contract.allowedProviders.includes(request.provider)) throw new ModelContractViolation(\"provider-not-allowed\");\n\tif (!contract.allowedModels.some((entry) => entry.provider === request.provider && entry.id === request.model.id)) {\n\t\tthrow new ModelContractViolation(\"model-not-allowed\");\n\t}\n\tif (!contract.allowedAuthOrigins.includes(request.authOrigin ?? request.provider)) {\n\t\tthrow new ModelContractViolation(\"auth-origin-not-allowed\");\n\t}\n\tif (request.thinking && !contract.thinking) throw new ModelContractViolation(\"thinking-not-allowed\");\n\tconst effectiveLevel = request.thinkingLevel ?? \"off\";\n\tif (contract.thinkingLevel !== undefined && contract.thinkingLevel !== effectiveLevel) {\n\t\tthrow new ModelContractViolation(\"thinking-level-mismatch\");\n\t}\n\tif (request.maxOutputTokens !== undefined && request.maxOutputTokens > contract.maxOutputTokens) {\n\t\tthrow new ModelContractViolation(\"output-limit-exceeded\");\n\t}\n}\n\n/** Copy only policy fields, never full model metadata or credential-bearing headers. */\nexport function snapshotModelContract(contract: unknown): ModelContract {\n\tvalidateContract(contract);\n\treturn createImmutableSnapshot({\n\t\tallowedModels: contract.allowedModels.map(({ provider, id }) => ({ provider, id })),\n\t\tallowedProviders: [...contract.allowedProviders],\n\t\tallowedAuthOrigins: [...contract.allowedAuthOrigins],\n\t\tthinking: contract.thinking,\n\t\tmaxOutputTokens: contract.maxOutputTokens,\n\t\t...(contract.thinkingLevel === undefined ? {} : { thinkingLevel: contract.thinkingLevel }),\n\t});\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AssistantMessage, AssistantMessageEvent, ImageContent, Message, Model, SimpleStreamOptions, streamSimple, TextContent, Tool, ToolResultMessage } from "omk-ai";
2
2
  import type { Static, TSchema } from "typebox";
3
+ import type { ModelContract, ProviderRequestEvent } from "./provider-request-types.ts";
3
4
  /**
4
5
  * Stream function used by the agent loop.
5
6
  *
@@ -126,6 +127,8 @@ export interface PrepareNextTurnContext extends ShouldStopAfterTurnContext {
126
127
  }
127
128
  export interface AgentLoopConfig extends SimpleStreamOptions {
128
129
  model: Model<any>;
130
+ /** Optional logical dispatch contract, pinned for the loop invocation. Not a wire attestation. */
131
+ modelContract?: ModelContract;
129
132
  /**
130
133
  * Maximum provider turns allowed in one loop invocation.
131
134
  *
@@ -573,7 +576,7 @@ export interface AgentContext {
573
576
  * part of run settlement. `tool_execution_update` delivery is observation-only:
574
577
  * subscriber promises are detached and cannot delay timeout/abort terminality.
575
578
  */
576
- export type AgentEvent = {
579
+ export type AgentEvent = ProviderRequestEvent | {
577
580
  type: "agent_start";
578
581
  } | {
579
582
  type: "agent_end";