@graphorin/provider 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +7 -7
  3. package/dist/adapters/llamacpp-server.d.ts +1 -1
  4. package/dist/adapters/llamacpp-server.js +1 -1
  5. package/dist/adapters/llamacpp-server.js.map +1 -1
  6. package/dist/adapters/ollama.d.ts.map +1 -1
  7. package/dist/adapters/ollama.js +3 -2
  8. package/dist/adapters/ollama.js.map +1 -1
  9. package/dist/adapters/openai-compatible.d.ts +14 -1
  10. package/dist/adapters/openai-compatible.d.ts.map +1 -1
  11. package/dist/adapters/openai-compatible.js +2 -0
  12. package/dist/adapters/openai-compatible.js.map +1 -1
  13. package/dist/adapters/vercel-messages.js +245 -0
  14. package/dist/adapters/vercel-messages.js.map +1 -0
  15. package/dist/adapters/vercel.d.ts +10 -8
  16. package/dist/adapters/vercel.d.ts.map +1 -1
  17. package/dist/adapters/vercel.js +54 -14
  18. package/dist/adapters/vercel.js.map +1 -1
  19. package/dist/counters/anthropic-wire.js +149 -0
  20. package/dist/counters/anthropic-wire.js.map +1 -0
  21. package/dist/counters/anthropic.d.ts +6 -1
  22. package/dist/counters/anthropic.d.ts.map +1 -1
  23. package/dist/counters/anthropic.js +25 -4
  24. package/dist/counters/anthropic.js.map +1 -1
  25. package/dist/counters/bedrock.js.map +1 -1
  26. package/dist/counters/dispatcher.d.ts.map +1 -1
  27. package/dist/counters/dispatcher.js +6 -4
  28. package/dist/counters/dispatcher.js.map +1 -1
  29. package/dist/counters/google.js.map +1 -1
  30. package/dist/counters/js-tiktoken.js.map +1 -1
  31. package/dist/errors/errors.d.ts +39 -11
  32. package/dist/errors/errors.d.ts.map +1 -1
  33. package/dist/errors/errors.js +54 -11
  34. package/dist/errors/errors.js.map +1 -1
  35. package/dist/errors/index.d.ts +2 -2
  36. package/dist/errors/index.js +2 -2
  37. package/dist/index.d.ts +5 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -5
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/abort.js +2 -2
  42. package/dist/internal/abort.js.map +1 -1
  43. package/dist/internal/http.js +19 -4
  44. package/dist/internal/http.js.map +1 -1
  45. package/dist/internal/openai-shaped.js +8 -3
  46. package/dist/internal/openai-shaped.js.map +1 -1
  47. package/dist/middleware/compose.d.ts +6 -6
  48. package/dist/middleware/compose.js +7 -7
  49. package/dist/middleware/compose.js.map +1 -1
  50. package/dist/middleware/production-hook.js.map +1 -1
  51. package/dist/middleware/with-cost-limit.d.ts +1 -1
  52. package/dist/middleware/with-cost-limit.js.map +1 -1
  53. package/dist/middleware/with-cost-tracking.d.ts +17 -4
  54. package/dist/middleware/with-cost-tracking.d.ts.map +1 -1
  55. package/dist/middleware/with-cost-tracking.js +12 -2
  56. package/dist/middleware/with-cost-tracking.js.map +1 -1
  57. package/dist/middleware/with-fallback.js +4 -2
  58. package/dist/middleware/with-fallback.js.map +1 -1
  59. package/dist/middleware/with-rate-limit.d.ts +2 -2
  60. package/dist/middleware/with-rate-limit.js.map +1 -1
  61. package/dist/middleware/with-redaction.d.ts +1 -1
  62. package/dist/middleware/with-redaction.js.map +1 -1
  63. package/dist/middleware/with-retry.js +3 -1
  64. package/dist/middleware/with-retry.js.map +1 -1
  65. package/dist/middleware/with-tracing.js +27 -6
  66. package/dist/middleware/with-tracing.js.map +1 -1
  67. package/dist/model-tier/classify.d.ts +1 -1
  68. package/dist/model-tier/classify.js +13 -5
  69. package/dist/model-tier/classify.js.map +1 -1
  70. package/dist/provider.js.map +1 -1
  71. package/dist/reasoning/apply-policy.js.map +1 -1
  72. package/dist/reasoning/classify-contract.js +14 -5
  73. package/dist/reasoning/classify-contract.js.map +1 -1
  74. package/dist/reasoning/retention.js.map +1 -1
  75. package/dist/tool-examples.d.ts +1 -1
  76. package/dist/tool-examples.js +1 -1
  77. package/dist/tool-examples.js.map +1 -1
  78. package/dist/trust/classify-local-provider.d.ts +1 -1
  79. package/dist/trust/classify-local-provider.js.map +1 -1
  80. package/package.json +6 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # @graphorin/provider
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#136](https://github.com/o-stepper/graphorin/pull/136) [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84) Thanks [@o-stepper](https://github.com/o-stepper)! - Prompt-cache economics end-to-end (audit 2026-07-04 Wave C, cluster C1).
8
+
9
+ - `Usage` gains `cachedReadTokens` / `cacheWriteTokens` (subsets of `promptTokens`), mapped by the vercel adapter (v7 `inputTokenDetails`; reasoning split kept exclusive of `completionTokens`) and the OpenAI-compatible adapter (`prompt_tokens_details.cached_tokens`); the fields flow through step/run aggregation, `usageByModel`, run-state (de)serialization and `withCostTracking` (new `cachedReadPerMtok`/`cacheWritePerMtok` lookup rates, full-input-rate fallback).
10
+ - New opt-in `ProviderRequest.cachePolicy` / `AgentConfig.cachePolicy` (`{ breakpoints: 'auto', ttl? }`): the vercel adapter anchors Anthropic `cache_control` on the first and last conversation messages so the stable prefix is written once and read at the discounted rate every later step.
11
+ - `ModelPrice` gains `cacheWriteUsdPerToken`; `calculateCost` bills cache writes; the bundled pricing snapshot is regenerated (2026-07-04) with current Anthropic 4.x / OpenAI gpt-5 + gpt-4.1 + o3/o4-mini / Gemini 2.5 families (legacy ids retained), `lookupPrice` resolves dated ids via a date-suffix fallback, and a new snapshot-coverage release gate cross-checks the model-tier classifier against the snapshot with an explicit known-unpriced allowlist (post-cutoff models report null cost + WARN instead of invented numbers).
12
+ - Cache-friendly catalogue: handoff tools serialize BEFORE the growing promoted section (byte-stable prefix under `tool_search` promotions), and new `toolPromotion: 'run-boundary'` freezes the advertised catalogue for a whole run while still persisting discoveries.
13
+
14
+ - [#136](https://github.com/o-stepper/graphorin/pull/136) [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84) Thanks [@o-stepper](https://github.com/o-stepper)! - Tools/MCP alignment (audit 2026-07-04 Wave C, cluster C2).
15
+
16
+ - Worked examples now fold into the model-facing tool description inside the adapters themselves (vercel, openai-shaped, ollama; idempotent with the `createProvider` fold), so raw-adapter setups get them too.
17
+ - `searchDeferred` indexes name + description + tags + worked-example comments (BM25 and semantic legs), widening `tool_search` recall to the phrasing examples document.
18
+ - tools-06: `sandbox_violation` and `rate_limited` are now actually produced - a non-ok `SandboxResult` keeps its structured kind (violation/memory-exceeded -> `sandbox_violation`, sandbox timeout -> `timeout`), and tool authors can throw the new `ToolRateLimitError` (with `retryAfterMs`) to surface `rate_limited` with a pacing hint.
19
+ - SEP-1303 conformance suite: one test per producible `ToolErrorKind` pins that every failure returns as a model-visible outcome (never a protocol throw, batch never shrinks). Docs: MCP 2026-07-28 RC deprecation callout (sampling/roots/logging frozen; Tasks targets the extension shape).
20
+
21
+ - [#136](https://github.com/o-stepper/graphorin/pull/136) [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84) Thanks [@o-stepper](https://github.com/o-stepper)! - Trace-tree observability (audit 2026-07-04 Wave C, cluster C7; pairs periphery-04).
22
+
23
+ The agent loop now emits the previously-declared `agent.run` span per run and `agent.step` spans per step (parented under the run); `tool.execute` parents under the current step via the new optional `RunContext.span`; a `withTracing`-wrapped provider parents under the step via the new `ProviderRequest.parentSpan` (a live handle like `signal`). Attributes align to the OTel GenAI semantic conventions (`gen_ai.operation.name`, `gen_ai.agent.id/name`, `gen_ai.tool.name`, `gen_ai.request.model`, `gen_ai.usage.input_tokens/output_tokens`), parent-based sampling finally has parents to follow, and observability.md documents the real tree plus the memory-tier-spans-not-yet-parented limitation.
24
+
25
+ ### Patch Changes
26
+
27
+ - [#135](https://github.com/o-stepper/graphorin/pull/135) [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3) Thanks [@o-stepper](https://github.com/o-stepper)! - Provider adapters now match their real SDK / wire contracts (audit 2026-07-04 Wave B, cluster B1).
28
+
29
+ - vercel adapter: Graphorin tools convert to the AI SDK's name-keyed record with `jsonSchema()`-shaped input schemas; assistant `toolCalls` become `tool-call` content parts, `ToolMessage`s become `tool-result` messages, system-role messages hoist into the `system` option, and `toolChoice` maps onto the SDK spelling. Tool loops now run against the real `ai` peer (previously every tool conversation failed SDK validation). A real-SDK contract test suite (dev-only `ai` dependency) pins the shapes.
30
+ - Anthropic token counter posts Anthropic wire-shaped bodies (system hoist, `tool_use` / `tool_result` blocks, turn merging) instead of raw Graphorin messages that 400'd on any agent transcript; degradation to the tiktoken fallback now WARNs once.
31
+ - HTTP errors carry a canonical `errorKind` (shared `classifyHttpStatus` mapper: 429 rate-limit, 401/403 unauthorized, 5xx transient/capacity, context-length body sniff) plus captured `retry-after` / `x-ratelimit-*` headers; `withFallback` / `withRetry` consult them, so a 429 on the primary finally fails over and honours server-provided delays.
32
+ - llamacpp-node: the system prompt is no longer injected twice, per-request contexts/sequences are disposed after every stream (KV-cache leak), and aborted streams report `finishReason: 'aborted'`.
33
+ - OpenAI-compatible streaming sends `stream_options: { include_usage: true }` so vLLM / Together / OpenAI report real usage; `openAICompatibleAdapter` gained the `capabilities` / `timeoutMs` options its siblings had.
34
+ - `withCostTracking` bills separately-reported reasoning tokens at the output rate; classifiers recognise Bedrock cross-region ids (`us.anthropic.claude-...`) and the AI SDK's dotted provider ids.
35
+
36
+ - Updated dependencies [[`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`6293a25`](https://github.com/o-stepper/graphorin/commit/6293a2531c5c9265294da22fb365a08f2bdf445a)]:
37
+ - @graphorin/core@0.6.0
38
+ - @graphorin/observability@0.6.0
39
+
40
+ ## 0.5.0
41
+
42
+ First version published to the npm registry (with Sigstore build
43
+ provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
44
+ milestones and were never published. All `@graphorin/*` packages release
45
+ lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
46
+ in the repository-level
47
+ [CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
48
+
3
49
  ## 0.1.0
4
50
 
5
51
  Initial release. See the root `CHANGELOG.md` and the corresponding
package/README.md CHANGED
@@ -5,19 +5,19 @@
5
5
 
6
6
  The package owns four moving parts:
7
7
 
8
- 1. **`createProvider(...)`** wraps any adapter in a stable `Provider`
8
+ 1. **`createProvider(...)`** - wraps any adapter in a stable `Provider`
9
9
  shape with sensitivity, capability, and reasoning-retention defaults.
10
- 2. **Adapters** `vercelAdapter` (default cloud path; wraps the Vercel
10
+ 2. **Adapters** - `vercelAdapter` (default cloud path; wraps the Vercel
11
11
  AI SDK), `ollamaAdapter` (direct Ollama HTTP), `llamaCppServerAdapter`
12
12
  (the upstream `llama-server` binary from llama.cpp), and
13
13
  `openAICompatibleAdapter` (LMStudio / LocalAI / vLLM / Together-style
14
14
  self-host endpoints).
15
- 3. **Middleware** `composeProviderMiddleware([...])` enforces a
15
+ 3. **Middleware** - `composeProviderMiddleware([...])` enforces a
16
16
  canonical order at startup and throws `MiddlewareOrderingError` on
17
17
  violation. Built-ins: `withTracing`, `withRetry`, `withRateLimit`,
18
18
  `withCostLimit`, `withCostTracking`, `withFallback`, and
19
19
  `withRedaction` (mandatory in production).
20
- 4. **Token counting** pluggable `TokenCounter` dispatcher. Default
20
+ 4. **Token counting** - pluggable `TokenCounter` dispatcher. Default
21
21
  `JsTiktokenCounter` for OpenAI-compatible models; per-vendor native
22
22
  counters for Anthropic, Google, and Bedrock; heuristic fallback for
23
23
  unknown providers with a one-time WARN.
@@ -72,7 +72,7 @@ const safeProvider = composeProviderMiddleware([
72
72
  ```ts
73
73
  import { ollamaAdapter } from '@graphorin/provider/adapters/ollama';
74
74
 
75
- // Auto-classified as 'loopback' no warning, no first-run prompt.
75
+ // Auto-classified as 'loopback' - no warning, no first-run prompt.
76
76
  const local = createProvider(
77
77
  ollamaAdapter({ model: 'llama3.1:8b', baseUrl: 'http://127.0.0.1:11434' }),
78
78
  );
@@ -81,7 +81,7 @@ const local = createProvider(
81
81
  The same `LocalProviderTrust` classifier (`'loopback' | 'private' |
82
82
  'public-tls' | 'public-cleartext'`) drives the trust auto-detection,
83
83
  the sensitivity-tier defaults, and the `withRedaction` policy table
84
- for every `baseUrl`-driven adapter `ollamaAdapter`,
84
+ for every `baseUrl`-driven adapter - `ollamaAdapter`,
85
85
  `llamaCppServerAdapter`, and `openAICompatibleAdapter`. The classifier
86
86
  lives at `@graphorin/provider/trust`. Public-cleartext URLs refuse to
87
87
  start with `LocalProviderInsecureTransportError`.
@@ -101,5 +101,5 @@ recommendations.
101
101
 
102
102
  ## Project metadata
103
103
 
104
- - **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko
104
+ - **Project Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko
105
105
  - Repository: <https://github.com/o-stepper/graphorin>
@@ -47,7 +47,7 @@ interface LlamaCppServerAdapterOptions {
47
47
  }
48
48
  /**
49
49
  * Build a Graphorin {@link Provider} backed by the upstream
50
- * `llama-server` binary. The factory does not start the binary
50
+ * `llama-server` binary. The factory does not start the binary -
51
51
  * operators launch it themselves with the desired model + GPU flags
52
52
  * and pass the URL here.
53
53
  *
@@ -9,7 +9,7 @@ import { buildOpenAIShapedProvider } from "../internal/openai-shaped.js";
9
9
  const DEFAULT_LLAMACPP_SERVER_BASE_URL = "http://127.0.0.1:8080";
10
10
  /**
11
11
  * Build a Graphorin {@link Provider} backed by the upstream
12
- * `llama-server` binary. The factory does not start the binary
12
+ * `llama-server` binary. The factory does not start the binary -
13
13
  * operators launch it themselves with the desired model + GPU flags
14
14
  * and pass the URL here.
15
15
  *
@@ -1 +1 @@
1
- {"version":3,"file":"llamacpp-server.js","names":[],"sources":["../../src/adapters/llamacpp-server.ts"],"sourcesContent":["/**\n * Direct adapter for the upstream `llama-server` binary from the\n * llama.cpp project. The binary speaks the OpenAI-compatible REST\n * contract end-to-end (`POST /v1/chat/completions`, `POST /v1/completions`,\n * `POST /v1/embeddings`); streaming is via `text/event-stream` chunks\n * terminated by `data: [DONE]` exactly as the upstream OpenAI shape.\n *\n * The adapter shares a single `LocalProviderTrust` classifier with\n * `ollamaAdapter` and `openAICompatibleAdapter` one classifier, one\n * policy table, one error type.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, Sensitivity } from '@graphorin/core';\n\nimport { buildOpenAIShapedProvider } from '../internal/openai-shaped.js';\n\n/**\n * Default port used by the upstream `llama-server` binary.\n *\n * @stable\n */\nexport const DEFAULT_LLAMACPP_SERVER_BASE_URL = 'http://127.0.0.1:8080';\n\n/**\n * Options accepted by {@link llamaCppServerAdapter}.\n *\n * @stable\n */\nexport interface LlamaCppServerAdapterOptions {\n /** GGUF model identifier exposed by the running server (e.g. `'qwen2.5:7b-instruct-q4_k_m'`). */\n readonly model: string;\n /** Base URL of the running `llama-server` process. Defaults to `http://127.0.0.1:8080`. */\n readonly baseUrl?: string;\n /** Optional bearer-auth API key (`--api-key` flag on the server). */\n readonly apiKey?: string;\n /** Extra headers merged on top of `content-type` + `accept` defaults. */\n readonly headers?: Readonly<Record<string, string>>;\n /** Custom `fetch` implementation; useful for tests. */\n readonly fetchImpl?: typeof fetch;\n /**\n * Time-to-response budget per request (PS-24). Default\n * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.\n */\n readonly timeoutMs?: number;\n /** Capability overrides merged on top of the adapter defaults. */\n readonly capabilities?: Partial<import('@graphorin/core').ProviderCapabilities>;\n /**\n * Acknowledge the risk of running over plaintext HTTP against a\n * public host. Without this flag the adapter throws\n * {@link import('../errors/errors.js').LocalProviderInsecureTransportError}.\n */\n readonly allowInsecureTransport?: boolean;\n /** Override for the default `acceptsSensitivity` value. */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /** Provider name attached to spans / log lines. */\n readonly name?: string;\n /** Optional log sink. Tests pass a fixture sink to silence the console. */\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\n/**\n * Build a Graphorin {@link Provider} backed by the upstream\n * `llama-server` binary. The factory does not start the binary —\n * operators launch it themselves with the desired model + GPU flags\n * and pass the URL here.\n *\n * @example\n * ```ts\n * const local = createProvider(\n * llamaCppServerAdapter({\n * model: 'qwen2.5:7b-instruct-q4_k_m',\n * baseUrl: 'http://127.0.0.1:8080',\n * }),\n * );\n * ```\n *\n * @stable\n */\nexport function llamaCppServerAdapter(options: LlamaCppServerAdapterOptions): Provider {\n const baseUrl = options.baseUrl ?? DEFAULT_LLAMACPP_SERVER_BASE_URL;\n const providerName = options.name ?? `llamacpp-server-${options.model}`;\n const built = buildOpenAIShapedProvider({\n providerName,\n model: options.model,\n baseUrl,\n ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),\n ...(options.headers !== undefined ? { headers: options.headers } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.allowInsecureTransport !== undefined\n ? { allowInsecureTransport: options.allowInsecureTransport }\n : {}),\n ...(options.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: options.acceptsSensitivity }\n : {}),\n ...(options.logger !== undefined ? { logger: options.logger } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n capabilities: { multimodal: false, parallelToolCalls: false, ...options.capabilities },\n });\n return built.provider;\n}\n"],"mappings":";;;;;;;;AAuBA,MAAa,mCAAmC;;;;;;;;;;;;;;;;;;;AAyDhD,SAAgB,sBAAsB,SAAiD;CACrF,MAAM,UAAU,QAAQ,WAAW;AAmBnC,QAjBc,0BAA0B;EACtC,cAFmB,QAAQ,QAAQ,mBAAmB,QAAQ;EAG9D,OAAO,QAAQ;EACf;EACA,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,GAAI,QAAQ,YAAY,SAAY,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;EACrE,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,GAAI,QAAQ,2BAA2B,SACnC,EAAE,wBAAwB,QAAQ,wBAAwB,GAC1D,EAAE;EACN,GAAI,QAAQ,uBAAuB,SAC/B,EAAE,oBAAoB,QAAQ,oBAAoB,GAClD,EAAE;EACN,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,cAAc;GAAE,YAAY;GAAO,mBAAmB;GAAO,GAAG,QAAQ;GAAc;EACvF,CAAC,CACW"}
1
+ {"version":3,"file":"llamacpp-server.js","names":[],"sources":["../../src/adapters/llamacpp-server.ts"],"sourcesContent":["/**\n * Direct adapter for the upstream `llama-server` binary from the\n * llama.cpp project. The binary speaks the OpenAI-compatible REST\n * contract end-to-end (`POST /v1/chat/completions`, `POST /v1/completions`,\n * `POST /v1/embeddings`); streaming is via `text/event-stream` chunks\n * terminated by `data: [DONE]` exactly as the upstream OpenAI shape.\n *\n * The adapter shares a single `LocalProviderTrust` classifier with\n * `ollamaAdapter` and `openAICompatibleAdapter` - one classifier, one\n * policy table, one error type.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, Sensitivity } from '@graphorin/core';\n\nimport { buildOpenAIShapedProvider } from '../internal/openai-shaped.js';\n\n/**\n * Default port used by the upstream `llama-server` binary.\n *\n * @stable\n */\nexport const DEFAULT_LLAMACPP_SERVER_BASE_URL = 'http://127.0.0.1:8080';\n\n/**\n * Options accepted by {@link llamaCppServerAdapter}.\n *\n * @stable\n */\nexport interface LlamaCppServerAdapterOptions {\n /** GGUF model identifier exposed by the running server (e.g. `'qwen2.5:7b-instruct-q4_k_m'`). */\n readonly model: string;\n /** Base URL of the running `llama-server` process. Defaults to `http://127.0.0.1:8080`. */\n readonly baseUrl?: string;\n /** Optional bearer-auth API key (`--api-key` flag on the server). */\n readonly apiKey?: string;\n /** Extra headers merged on top of `content-type` + `accept` defaults. */\n readonly headers?: Readonly<Record<string, string>>;\n /** Custom `fetch` implementation; useful for tests. */\n readonly fetchImpl?: typeof fetch;\n /**\n * Time-to-response budget per request (PS-24). Default\n * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.\n */\n readonly timeoutMs?: number;\n /** Capability overrides merged on top of the adapter defaults. */\n readonly capabilities?: Partial<import('@graphorin/core').ProviderCapabilities>;\n /**\n * Acknowledge the risk of running over plaintext HTTP against a\n * public host. Without this flag the adapter throws\n * {@link import('../errors/errors.js').LocalProviderInsecureTransportError}.\n */\n readonly allowInsecureTransport?: boolean;\n /** Override for the default `acceptsSensitivity` value. */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /** Provider name attached to spans / log lines. */\n readonly name?: string;\n /** Optional log sink. Tests pass a fixture sink to silence the console. */\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\n/**\n * Build a Graphorin {@link Provider} backed by the upstream\n * `llama-server` binary. The factory does not start the binary -\n * operators launch it themselves with the desired model + GPU flags\n * and pass the URL here.\n *\n * @example\n * ```ts\n * const local = createProvider(\n * llamaCppServerAdapter({\n * model: 'qwen2.5:7b-instruct-q4_k_m',\n * baseUrl: 'http://127.0.0.1:8080',\n * }),\n * );\n * ```\n *\n * @stable\n */\nexport function llamaCppServerAdapter(options: LlamaCppServerAdapterOptions): Provider {\n const baseUrl = options.baseUrl ?? DEFAULT_LLAMACPP_SERVER_BASE_URL;\n const providerName = options.name ?? `llamacpp-server-${options.model}`;\n const built = buildOpenAIShapedProvider({\n providerName,\n model: options.model,\n baseUrl,\n ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),\n ...(options.headers !== undefined ? { headers: options.headers } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.allowInsecureTransport !== undefined\n ? { allowInsecureTransport: options.allowInsecureTransport }\n : {}),\n ...(options.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: options.acceptsSensitivity }\n : {}),\n ...(options.logger !== undefined ? { logger: options.logger } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n capabilities: { multimodal: false, parallelToolCalls: false, ...options.capabilities },\n });\n return built.provider;\n}\n"],"mappings":";;;;;;;;AAuBA,MAAa,mCAAmC;;;;;;;;;;;;;;;;;;;AAyDhD,SAAgB,sBAAsB,SAAiD;CACrF,MAAM,UAAU,QAAQ,WAAW;AAmBnC,QAjBc,0BAA0B;EACtC,cAFmB,QAAQ,QAAQ,mBAAmB,QAAQ;EAG9D,OAAO,QAAQ;EACf;EACA,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,GAAI,QAAQ,YAAY,SAAY,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;EACrE,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,GAAI,QAAQ,2BAA2B,SACnC,EAAE,wBAAwB,QAAQ,wBAAwB,GAC1D,EAAE;EACN,GAAI,QAAQ,uBAAuB,SAC/B,EAAE,oBAAoB,QAAQ,oBAAoB,GAClD,EAAE;EACN,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,cAAc;GAAE,YAAY;GAAO,mBAAmB;GAAO,GAAG,QAAQ;GAAc;EACvF,CAAC,CACW"}
@@ -1 +1 @@
1
- {"version":3,"file":"ollama.d.ts","names":[],"sources":["../../src/adapters/ollama.ts"],"sourcesContent":[],"mappings":";;;;;AAoFA;;;;cA5Ca,uBAAA;;;;;;UAOI,oBAAA;;;;qBAII,SAAS;8BACA;;;;;;;gCAOE,cAAc;0BACpB,QAAQ;;;;;;;;;;;iBAwBlB,aAAA,UAAuB,uBAAuB"}
1
+ {"version":3,"file":"ollama.d.ts","names":[],"sources":["../../src/adapters/ollama.ts"],"sourcesContent":[],"mappings":";;;;;AAqFA;;;;cA5Ca,uBAAA;;;;;;UAOI,oBAAA;;;;qBAII,SAAS;8BACA;;;;;;;gCAOE,cAAc;0BACpB,QAAQ;;;;;;;;;;;iBAwBlB,aAAA,UAAuB,uBAAuB"}
@@ -1,6 +1,7 @@
1
1
  import { LocalProviderInsecureTransportError, ProviderStreamParseError } from "../errors/errors.js";
2
2
  import { applyReasoningPolicy } from "../reasoning/apply-policy.js";
3
3
  import { resolveReasoningRetention } from "../reasoning/retention.js";
4
+ import { foldToolExamples } from "../tool-examples.js";
4
5
  import { classifyLocalProvider } from "../trust/classify-local-provider.js";
5
6
  import { callJsonHttp, makeStreamStartEvent, toOllamaChatMessages } from "../internal/http.js";
6
7
  import { parseNdJsonStream } from "../internal/sse.js";
@@ -13,7 +14,7 @@ import { randomUUID } from "node:crypto";
13
14
  * native Ollama streaming JSON protocol (`POST /api/chat` returning
14
15
  * newline-delimited JSON objects). For operators who prefer the
15
16
  * OpenAI-compatible variant exposed by recent Ollama releases, the
16
- * generic {@link openAICompatibleAdapter} is the better choice both
17
+ * generic {@link openAICompatibleAdapter} is the better choice - both
17
18
  * adapters share the same {@link LocalProviderTrust} classifier and
18
19
  * {@link LocalProviderInsecureTransportError} startup behaviour.
19
20
  *
@@ -191,7 +192,7 @@ function buildBody(model, req, stream, structuredOutput) {
191
192
  if (req.maxTokens !== void 0) optionsBlock.num_predict = req.maxTokens;
192
193
  body.options = optionsBlock;
193
194
  }
194
- if (req.tools !== void 0 && req.tools.length > 0) body.tools = req.tools.map((t) => ({
195
+ if (req.tools !== void 0 && req.tools.length > 0) body.tools = foldToolExamples(req.tools).map((t) => ({
195
196
  type: "function",
196
197
  function: {
197
198
  name: t.name,
@@ -1 +1 @@
1
- {"version":3,"file":"ollama.js","names":["DEFAULT_CAPABILITIES: ProviderCapabilities","capabilities: ProviderCapabilities","usage: Usage","finishReason: FinishReason","chunk: OllamaChatChunk","json: OllamaChatChunk","body: Record<string, unknown>","optionsBlock: Record<string, unknown>"],"sources":["../../src/adapters/ollama.ts"],"sourcesContent":["/**\n * Direct adapter for the Ollama HTTP API. The adapter speaks the\n * native Ollama streaming JSON protocol (`POST /api/chat` returning\n * newline-delimited JSON objects). For operators who prefer the\n * OpenAI-compatible variant exposed by recent Ollama releases, the\n * generic {@link openAICompatibleAdapter} is the better choice — both\n * adapters share the same {@link LocalProviderTrust} classifier and\n * {@link LocalProviderInsecureTransportError} startup behaviour.\n *\n * @packageDocumentation\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n FinishReason,\n Provider,\n ProviderCapabilities,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n Usage,\n} from '@graphorin/core';\n\nimport { LocalProviderInsecureTransportError, ProviderStreamParseError } from '../errors/errors.js';\nimport { callJsonHttp, makeStreamStartEvent, toOllamaChatMessages } from '../internal/http.js';\nimport { parseNdJsonStream } from '../internal/sse.js';\nimport { stripTrailingSlashes } from '../internal/url-utils.js';\nimport { applyReasoningPolicy } from '../reasoning/apply-policy.js';\nimport { resolveReasoningRetention } from '../reasoning/retention.js';\nimport {\n classifyLocalProvider,\n type LocalProviderClassification,\n} from '../trust/classify-local-provider.js';\n\n/**\n * Default Ollama base URL.\n *\n * @stable\n */\nexport const DEFAULT_OLLAMA_BASE_URL = 'http://127.0.0.1:11434';\n\n/**\n * Options accepted by {@link ollamaAdapter}.\n *\n * @stable\n */\nexport interface OllamaAdapterOptions {\n readonly model: string;\n readonly baseUrl?: string;\n readonly chatPath?: string;\n readonly headers?: Readonly<Record<string, string>>;\n readonly fetchImpl?: typeof fetch;\n /**\n * Time-to-response budget per request (PS-24). Default\n * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.\n */\n readonly timeoutMs?: number;\n readonly allowInsecureTransport?: boolean;\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n readonly capabilities?: Partial<ProviderCapabilities>;\n readonly name?: string;\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n streaming: true,\n toolCalling: true,\n parallelToolCalls: false,\n multimodal: false,\n structuredOutput: true,\n reasoning: false,\n contextWindow: 8_192,\n maxOutput: 4_096,\n reasoningContract: 'optional',\n};\n\n/**\n * Build a Graphorin {@link Provider} backed by Ollama's native HTTP\n * API. The adapter is fail-safe by default: public-cleartext URLs\n * refuse to start with `LocalProviderInsecureTransportError`.\n *\n * @stable\n */\nexport function ollamaAdapter(options: OllamaAdapterOptions): Provider {\n const baseUrl = options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL;\n const classification = classifyLocalProvider(baseUrl);\n if (classification.trust === 'public-cleartext' && options.allowInsecureTransport !== true) {\n throw new LocalProviderInsecureTransportError(baseUrl);\n }\n const log = options.logger ?? defaultLogger;\n emitTrustWarning(log, classification, baseUrl);\n const acceptsSensitivity = options.acceptsSensitivity ?? classification.acceptsSensitivity;\n const providerName = options.name ?? `ollama-${options.model}`;\n const capabilities: ProviderCapabilities = {\n ...DEFAULT_CAPABILITIES,\n ...options.capabilities,\n };\n const chatPath = options.chatPath ?? '/api/chat';\n const url = `${stripTrailingSlashes(baseUrl)}${chatPath}`;\n return {\n name: providerName,\n modelId: options.model,\n capabilities,\n acceptsSensitivity,\n stream(req) {\n return streamOllama(options, providerName, url, applyOllamaPreflight(req, capabilities));\n },\n async generate(req) {\n return generateOllama(options, providerName, url, applyOllamaPreflight(req, capabilities));\n },\n };\n}\n\nfunction applyOllamaPreflight(\n req: ProviderRequest,\n capabilities: ProviderCapabilities,\n): ProviderRequest {\n const retention = resolveReasoningRetention({\n ...(req.reasoningRetention !== undefined ? { requested: req.reasoningRetention } : {}),\n ...(capabilities.reasoningContract !== undefined\n ? { contract: capabilities.reasoningContract }\n : {}),\n });\n if (retention === 'pass-through-all') return req;\n const filtered = applyReasoningPolicy({ messages: req.messages, retention });\n if (filtered === req.messages) return req;\n return { ...req, messages: filtered };\n}\n\nasync function* streamOllama(\n options: OllamaAdapterOptions,\n providerName: string,\n url: string,\n req: ProviderRequest,\n): AsyncIterable<ProviderEvent> {\n const body = buildBody(options.model, req, true, options.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName,\n url,\n headers: buildHeaders(options),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n yield makeStreamStartEvent({ providerName, modelId: options.model });\n let usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n let finishReason: FinishReason = 'stop';\n for await (const line of parseNdJsonStream(\n resp.body,\n req.signal !== undefined ? { signal: req.signal } : {},\n )) {\n if (req.signal?.aborted) {\n finishReason = 'aborted'; // PS-12: honest abort reason, not 'stop'\n break;\n }\n let chunk: OllamaChatChunk;\n try {\n chunk = JSON.parse(line) as OllamaChatChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(\n providerName,\n `failed to parse ndjson line: ${(cause as Error).message}`,\n cause,\n );\n }\n if (chunk.message?.content !== undefined && chunk.message.content.length > 0) {\n yield { type: 'text-delta', delta: chunk.message.content };\n }\n if (Array.isArray(chunk.message?.tool_calls)) {\n for (const tc of chunk.message.tool_calls) {\n const toolCallId = tc.id ?? `call_${randomUUID()}`;\n const toolName = tc.function?.name ?? '';\n if (toolName.length === 0) continue;\n yield { type: 'tool-call-start', toolCallId, toolName };\n yield {\n type: 'tool-call-end',\n toolCallId,\n finalArgs: tc.function?.arguments ?? {},\n };\n }\n }\n if (chunk.done === true) {\n finishReason = mapFinishReason(chunk.done_reason);\n usage = mapUsage(chunk);\n break;\n }\n }\n yield { type: 'finish', finishReason, usage };\n}\n\nasync function generateOllama(\n options: OllamaAdapterOptions,\n providerName: string,\n url: string,\n req: ProviderRequest,\n): Promise<ProviderResponse> {\n const body = buildBody(options.model, req, false, options.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName,\n url,\n headers: buildHeaders(options),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n let json: OllamaChatChunk;\n try {\n json = (await resp.json()) as OllamaChatChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(providerName, 'response body was not valid JSON', cause);\n }\n return {\n usage: mapUsage(json),\n finishReason: mapFinishReason(json.done_reason),\n ...(typeof json.message?.content === 'string' && json.message.content.length > 0\n ? { text: json.message.content }\n : {}),\n ...(Array.isArray(json.message?.tool_calls) && json.message.tool_calls.length > 0\n ? {\n toolCalls: json.message.tool_calls.map((tc) => ({\n toolCallId: tc.id ?? `call_${randomUUID()}`,\n toolName: tc.function?.name ?? '',\n args: tc.function?.arguments ?? {},\n })),\n }\n : {}),\n };\n}\n\nfunction buildBody(\n model: string,\n req: ProviderRequest,\n stream: boolean,\n structuredOutput: boolean,\n): Record<string, unknown> {\n const messages =\n req.systemMessage !== undefined\n ? [{ role: 'system' as const, content: req.systemMessage }, ...req.messages]\n : req.messages;\n const body: Record<string, unknown> = {\n model,\n messages: toOllamaChatMessages(messages),\n stream,\n };\n if (req.temperature !== undefined || req.maxTokens !== undefined) {\n const optionsBlock: Record<string, unknown> = {};\n if (req.temperature !== undefined) optionsBlock.temperature = req.temperature;\n if (req.maxTokens !== undefined) optionsBlock.num_predict = req.maxTokens;\n body.options = optionsBlock;\n }\n if (req.tools !== undefined && req.tools.length > 0) {\n body.tools = req.tools.map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n parameters: t.inputSchema,\n },\n }));\n }\n // PS-24: Ollama's native structured output — `format` takes a JSON\n // schema object (or 'json' for schema-less JSON mode).\n if (structuredOutput && req.outputType?.kind === 'structured') {\n body.format = req.outputType.jsonSchema ?? 'json';\n }\n if (req.providerOptions !== undefined) {\n Object.assign(body, req.providerOptions);\n }\n return body;\n}\n\nfunction buildHeaders(options: OllamaAdapterOptions): Record<string, string> {\n return {\n 'content-type': 'application/json',\n accept: 'application/json',\n ...options.headers,\n };\n}\n\nfunction mapFinishReason(value: string | null | undefined): FinishReason {\n switch (value) {\n case 'stop':\n case 'length':\n return value;\n case 'tool_calls':\n return 'tool-calls';\n default:\n return 'stop';\n }\n}\n\nfunction mapUsage(chunk: OllamaChatChunk): Usage {\n const promptTokens = chunk.prompt_eval_count ?? 0;\n const completionTokens = chunk.eval_count ?? 0;\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n };\n}\n\nfunction emitTrustWarning(\n log: (level: 'warn' | 'info', message: string, meta?: object) => void,\n classification: LocalProviderClassification,\n baseUrl: string,\n): void {\n if (classification.trust === 'public-cleartext') {\n log('warn', `[ollama] allowInsecureTransport=true accepted for ${baseUrl}`, { baseUrl });\n } else if (classification.trust === 'public-tls') {\n log('warn', `[ollama] public-TLS endpoint; treating as cloud-tier`, { baseUrl });\n } else if (classification.trust === 'private') {\n log('warn', `[ollama] private-network endpoint detected (${classification.reason})`, {\n baseUrl,\n acceptsSensitivity: classification.acceptsSensitivity,\n });\n }\n}\n\nfunction defaultLogger(level: 'warn' | 'info', message: string, meta?: object): void {\n const fn = level === 'warn' ? console.warn : console.info;\n if (meta !== undefined) {\n fn(`[graphorin/provider] ${message}`, meta);\n } else {\n fn(`[graphorin/provider] ${message}`);\n }\n}\n\ninterface OllamaChatChunk {\n readonly model?: string;\n readonly created_at?: string;\n readonly message?: {\n readonly role?: string;\n readonly content?: string;\n readonly tool_calls?: ReadonlyArray<{\n readonly id?: string;\n readonly function?: { readonly name?: string; readonly arguments?: unknown };\n }>;\n };\n readonly done?: boolean;\n readonly done_reason?: string;\n readonly prompt_eval_count?: number;\n readonly eval_count?: number;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ollama.js","names":["DEFAULT_CAPABILITIES: ProviderCapabilities","capabilities: ProviderCapabilities","usage: Usage","finishReason: FinishReason","chunk: OllamaChatChunk","json: OllamaChatChunk","body: Record<string, unknown>","optionsBlock: Record<string, unknown>"],"sources":["../../src/adapters/ollama.ts"],"sourcesContent":["/**\n * Direct adapter for the Ollama HTTP API. The adapter speaks the\n * native Ollama streaming JSON protocol (`POST /api/chat` returning\n * newline-delimited JSON objects). For operators who prefer the\n * OpenAI-compatible variant exposed by recent Ollama releases, the\n * generic {@link openAICompatibleAdapter} is the better choice - both\n * adapters share the same {@link LocalProviderTrust} classifier and\n * {@link LocalProviderInsecureTransportError} startup behaviour.\n *\n * @packageDocumentation\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type {\n FinishReason,\n Provider,\n ProviderCapabilities,\n ProviderEvent,\n ProviderRequest,\n ProviderResponse,\n Sensitivity,\n Usage,\n} from '@graphorin/core';\n\nimport { LocalProviderInsecureTransportError, ProviderStreamParseError } from '../errors/errors.js';\nimport { callJsonHttp, makeStreamStartEvent, toOllamaChatMessages } from '../internal/http.js';\nimport { parseNdJsonStream } from '../internal/sse.js';\nimport { stripTrailingSlashes } from '../internal/url-utils.js';\nimport { applyReasoningPolicy } from '../reasoning/apply-policy.js';\nimport { resolveReasoningRetention } from '../reasoning/retention.js';\nimport { foldToolExamples } from '../tool-examples.js';\nimport {\n classifyLocalProvider,\n type LocalProviderClassification,\n} from '../trust/classify-local-provider.js';\n\n/**\n * Default Ollama base URL.\n *\n * @stable\n */\nexport const DEFAULT_OLLAMA_BASE_URL = 'http://127.0.0.1:11434';\n\n/**\n * Options accepted by {@link ollamaAdapter}.\n *\n * @stable\n */\nexport interface OllamaAdapterOptions {\n readonly model: string;\n readonly baseUrl?: string;\n readonly chatPath?: string;\n readonly headers?: Readonly<Record<string, string>>;\n readonly fetchImpl?: typeof fetch;\n /**\n * Time-to-response budget per request (PS-24). Default\n * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.\n */\n readonly timeoutMs?: number;\n readonly allowInsecureTransport?: boolean;\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n readonly capabilities?: Partial<ProviderCapabilities>;\n readonly name?: string;\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n streaming: true,\n toolCalling: true,\n parallelToolCalls: false,\n multimodal: false,\n structuredOutput: true,\n reasoning: false,\n contextWindow: 8_192,\n maxOutput: 4_096,\n reasoningContract: 'optional',\n};\n\n/**\n * Build a Graphorin {@link Provider} backed by Ollama's native HTTP\n * API. The adapter is fail-safe by default: public-cleartext URLs\n * refuse to start with `LocalProviderInsecureTransportError`.\n *\n * @stable\n */\nexport function ollamaAdapter(options: OllamaAdapterOptions): Provider {\n const baseUrl = options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL;\n const classification = classifyLocalProvider(baseUrl);\n if (classification.trust === 'public-cleartext' && options.allowInsecureTransport !== true) {\n throw new LocalProviderInsecureTransportError(baseUrl);\n }\n const log = options.logger ?? defaultLogger;\n emitTrustWarning(log, classification, baseUrl);\n const acceptsSensitivity = options.acceptsSensitivity ?? classification.acceptsSensitivity;\n const providerName = options.name ?? `ollama-${options.model}`;\n const capabilities: ProviderCapabilities = {\n ...DEFAULT_CAPABILITIES,\n ...options.capabilities,\n };\n const chatPath = options.chatPath ?? '/api/chat';\n const url = `${stripTrailingSlashes(baseUrl)}${chatPath}`;\n return {\n name: providerName,\n modelId: options.model,\n capabilities,\n acceptsSensitivity,\n stream(req) {\n return streamOllama(options, providerName, url, applyOllamaPreflight(req, capabilities));\n },\n async generate(req) {\n return generateOllama(options, providerName, url, applyOllamaPreflight(req, capabilities));\n },\n };\n}\n\nfunction applyOllamaPreflight(\n req: ProviderRequest,\n capabilities: ProviderCapabilities,\n): ProviderRequest {\n const retention = resolveReasoningRetention({\n ...(req.reasoningRetention !== undefined ? { requested: req.reasoningRetention } : {}),\n ...(capabilities.reasoningContract !== undefined\n ? { contract: capabilities.reasoningContract }\n : {}),\n });\n if (retention === 'pass-through-all') return req;\n const filtered = applyReasoningPolicy({ messages: req.messages, retention });\n if (filtered === req.messages) return req;\n return { ...req, messages: filtered };\n}\n\nasync function* streamOllama(\n options: OllamaAdapterOptions,\n providerName: string,\n url: string,\n req: ProviderRequest,\n): AsyncIterable<ProviderEvent> {\n const body = buildBody(options.model, req, true, options.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName,\n url,\n headers: buildHeaders(options),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n yield makeStreamStartEvent({ providerName, modelId: options.model });\n let usage: Usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n let finishReason: FinishReason = 'stop';\n for await (const line of parseNdJsonStream(\n resp.body,\n req.signal !== undefined ? { signal: req.signal } : {},\n )) {\n if (req.signal?.aborted) {\n finishReason = 'aborted'; // PS-12: honest abort reason, not 'stop'\n break;\n }\n let chunk: OllamaChatChunk;\n try {\n chunk = JSON.parse(line) as OllamaChatChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(\n providerName,\n `failed to parse ndjson line: ${(cause as Error).message}`,\n cause,\n );\n }\n if (chunk.message?.content !== undefined && chunk.message.content.length > 0) {\n yield { type: 'text-delta', delta: chunk.message.content };\n }\n if (Array.isArray(chunk.message?.tool_calls)) {\n for (const tc of chunk.message.tool_calls) {\n const toolCallId = tc.id ?? `call_${randomUUID()}`;\n const toolName = tc.function?.name ?? '';\n if (toolName.length === 0) continue;\n yield { type: 'tool-call-start', toolCallId, toolName };\n yield {\n type: 'tool-call-end',\n toolCallId,\n finalArgs: tc.function?.arguments ?? {},\n };\n }\n }\n if (chunk.done === true) {\n finishReason = mapFinishReason(chunk.done_reason);\n usage = mapUsage(chunk);\n break;\n }\n }\n yield { type: 'finish', finishReason, usage };\n}\n\nasync function generateOllama(\n options: OllamaAdapterOptions,\n providerName: string,\n url: string,\n req: ProviderRequest,\n): Promise<ProviderResponse> {\n const body = buildBody(options.model, req, false, options.capabilities?.structuredOutput ?? true);\n const resp = await callJsonHttp({\n providerName,\n url,\n headers: buildHeaders(options),\n body,\n ...(req.signal !== undefined ? { signal: req.signal } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n });\n let json: OllamaChatChunk;\n try {\n json = (await resp.json()) as OllamaChatChunk;\n } catch (cause) {\n throw new ProviderStreamParseError(providerName, 'response body was not valid JSON', cause);\n }\n return {\n usage: mapUsage(json),\n finishReason: mapFinishReason(json.done_reason),\n ...(typeof json.message?.content === 'string' && json.message.content.length > 0\n ? { text: json.message.content }\n : {}),\n ...(Array.isArray(json.message?.tool_calls) && json.message.tool_calls.length > 0\n ? {\n toolCalls: json.message.tool_calls.map((tc) => ({\n toolCallId: tc.id ?? `call_${randomUUID()}`,\n toolName: tc.function?.name ?? '',\n args: tc.function?.arguments ?? {},\n })),\n }\n : {}),\n };\n}\n\nfunction buildBody(\n model: string,\n req: ProviderRequest,\n stream: boolean,\n structuredOutput: boolean,\n): Record<string, unknown> {\n const messages =\n req.systemMessage !== undefined\n ? [{ role: 'system' as const, content: req.systemMessage }, ...req.messages]\n : req.messages;\n const body: Record<string, unknown> = {\n model,\n messages: toOllamaChatMessages(messages),\n stream,\n };\n if (req.temperature !== undefined || req.maxTokens !== undefined) {\n const optionsBlock: Record<string, unknown> = {};\n if (req.temperature !== undefined) optionsBlock.temperature = req.temperature;\n if (req.maxTokens !== undefined) optionsBlock.num_predict = req.maxTokens;\n body.options = optionsBlock;\n }\n if (req.tools !== undefined && req.tools.length > 0) {\n // C2: fold worked examples in the adapter itself (idempotent when an\n // upstream createProvider fold already ran).\n body.tools = foldToolExamples(req.tools).map((t) => ({\n type: 'function',\n function: {\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n parameters: t.inputSchema,\n },\n }));\n }\n // PS-24: Ollama's native structured output - `format` takes a JSON\n // schema object (or 'json' for schema-less JSON mode).\n if (structuredOutput && req.outputType?.kind === 'structured') {\n body.format = req.outputType.jsonSchema ?? 'json';\n }\n if (req.providerOptions !== undefined) {\n Object.assign(body, req.providerOptions);\n }\n return body;\n}\n\nfunction buildHeaders(options: OllamaAdapterOptions): Record<string, string> {\n return {\n 'content-type': 'application/json',\n accept: 'application/json',\n ...options.headers,\n };\n}\n\nfunction mapFinishReason(value: string | null | undefined): FinishReason {\n switch (value) {\n case 'stop':\n case 'length':\n return value;\n case 'tool_calls':\n return 'tool-calls';\n default:\n return 'stop';\n }\n}\n\nfunction mapUsage(chunk: OllamaChatChunk): Usage {\n const promptTokens = chunk.prompt_eval_count ?? 0;\n const completionTokens = chunk.eval_count ?? 0;\n return {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n };\n}\n\nfunction emitTrustWarning(\n log: (level: 'warn' | 'info', message: string, meta?: object) => void,\n classification: LocalProviderClassification,\n baseUrl: string,\n): void {\n if (classification.trust === 'public-cleartext') {\n log('warn', `[ollama] allowInsecureTransport=true accepted for ${baseUrl}`, { baseUrl });\n } else if (classification.trust === 'public-tls') {\n log('warn', `[ollama] public-TLS endpoint; treating as cloud-tier`, { baseUrl });\n } else if (classification.trust === 'private') {\n log('warn', `[ollama] private-network endpoint detected (${classification.reason})`, {\n baseUrl,\n acceptsSensitivity: classification.acceptsSensitivity,\n });\n }\n}\n\nfunction defaultLogger(level: 'warn' | 'info', message: string, meta?: object): void {\n const fn = level === 'warn' ? console.warn : console.info;\n if (meta !== undefined) {\n fn(`[graphorin/provider] ${message}`, meta);\n } else {\n fn(`[graphorin/provider] ${message}`);\n }\n}\n\ninterface OllamaChatChunk {\n readonly model?: string;\n readonly created_at?: string;\n readonly message?: {\n readonly role?: string;\n readonly content?: string;\n readonly tool_calls?: ReadonlyArray<{\n readonly id?: string;\n readonly function?: { readonly name?: string; readonly arguments?: unknown };\n }>;\n };\n readonly done?: boolean;\n readonly done_reason?: string;\n readonly prompt_eval_count?: number;\n readonly eval_count?: number;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Provider, Sensitivity } from "@graphorin/core";
1
+ import { Provider, ProviderCapabilities, Sensitivity } from "@graphorin/core";
2
2
 
3
3
  //#region src/adapters/openai-compatible.d.ts
4
4
 
@@ -28,6 +28,19 @@ interface OpenAICompatibleAdapterOptions {
28
28
  * public host.
29
29
  */
30
30
  readonly allowInsecureTransport?: boolean;
31
+ /**
32
+ * Capability overrides merged on top of the adapter defaults
33
+ * (core-provider-10). Use them to widen `contextWindow` /
34
+ * `maxOutput` for large-context servers or to set
35
+ * `structuredOutput: false` for servers that reject
36
+ * `response_format`.
37
+ */
38
+ readonly capabilities?: Partial<ProviderCapabilities>;
39
+ /**
40
+ * Time-to-response budget per request (PS-24). Default 120s; `0`
41
+ * disables.
42
+ */
43
+ readonly timeoutMs?: number;
31
44
  /** Override for the default `acceptsSensitivity` value. */
32
45
  readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;
33
46
  /** Provider name attached to spans / log lines. */
@@ -1 +1 @@
1
- {"version":3,"file":"openai-compatible.d.ts","names":[],"sources":["../../src/adapters/openai-compatible.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAwBiB,8BAAA;;;;;;;;;;;;;qBAaI,SAAS;;8BAEA;;;;;;;gCAOE,cAAc;;;;;;;;;;;;;iBAc9B,uBAAA,UAAiC,iCAAiC"}
1
+ {"version":3,"file":"openai-compatible.d.ts","names":[],"sources":["../../src/adapters/openai-compatible.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UAwBiB,8BAAA;;;;;;;;;;;;;qBAaI,SAAS;;8BAEA;;;;;;;;;;;;;0BAaJ,QAAQ;;;;;;;gCAOF,cAAc;;;;;;;;;;;;;iBAc9B,uBAAA,UAAiC,iCAAiC"}
@@ -18,6 +18,8 @@ function openAICompatibleAdapter(options) {
18
18
  ...options.headers !== void 0 ? { headers: options.headers } : {},
19
19
  ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
20
20
  ...options.allowInsecureTransport !== void 0 ? { allowInsecureTransport: options.allowInsecureTransport } : {},
21
+ ...options.capabilities !== void 0 ? { capabilities: options.capabilities } : {},
22
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
21
23
  ...options.acceptsSensitivity !== void 0 ? { acceptsSensitivity: options.acceptsSensitivity } : {},
22
24
  ...options.logger !== void 0 ? { logger: options.logger } : {}
23
25
  }).provider;
@@ -1 +1 @@
1
- {"version":3,"file":"openai-compatible.js","names":[],"sources":["../../src/adapters/openai-compatible.ts"],"sourcesContent":["/**\n * Generic OpenAI-compatible adapter works against any HTTP server\n * that speaks the `/v1/chat/completions` REST contract. Tested\n * deployments include LMStudio (default port 1234), LocalAI (default\n * port 8080), vLLM (`python -m vllm.entrypoints.openai.api_server`,\n * default port 8000), Together-style self-host endpoints, and any\n * other server in the OpenAI-compatible ecosystem.\n *\n * The adapter shares the same `LocalProviderTrust` classifier as\n * `ollamaAdapter` and `llamaCppServerAdapter` one classifier, one\n * policy table, one error type.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, Sensitivity } from '@graphorin/core';\n\nimport { buildOpenAIShapedProvider } from '../internal/openai-shaped.js';\n\n/**\n * Options accepted by {@link openAICompatibleAdapter}.\n *\n * @stable\n */\nexport interface OpenAICompatibleAdapterOptions {\n /** Model identifier sent in the request body's `model` field. */\n readonly model: string;\n /**\n * Base URL of the OpenAI-compatible server. The classifier inspects\n * the protocol + host to assign a `LocalProviderTrust` value.\n */\n readonly baseUrl: string;\n /** Optional REST path override. Defaults to `/v1/chat/completions`. */\n readonly chatPath?: string;\n /** Optional bearer-auth API key. */\n readonly apiKey?: string;\n /** Extra headers merged on top of `content-type` + `accept` defaults. */\n readonly headers?: Readonly<Record<string, string>>;\n /** Custom `fetch` implementation; useful for tests. */\n readonly fetchImpl?: typeof fetch;\n /**\n * Acknowledge the risk of running over plaintext HTTP against a\n * public host.\n */\n readonly allowInsecureTransport?: boolean;\n /** Override for the default `acceptsSensitivity` value. */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /** Provider name attached to spans / log lines. */\n readonly name?: string;\n /** Optional log sink. */\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\n/**\n * Build a Graphorin {@link Provider} backed by an OpenAI-compatible\n * HTTP server. The same code path serves LMStudio, LocalAI, vLLM, and\n * any other compatible self-host endpoint.\n *\n * @stable\n */\nexport function openAICompatibleAdapter(options: OpenAICompatibleAdapterOptions): Provider {\n const providerName = options.name ?? `openai-compatible-${options.model}`;\n const built = buildOpenAIShapedProvider({\n providerName,\n model: options.model,\n baseUrl: options.baseUrl,\n ...(options.chatPath !== undefined ? { chatPath: options.chatPath } : {}),\n ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),\n ...(options.headers !== undefined ? { headers: options.headers } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.allowInsecureTransport !== undefined\n ? { allowInsecureTransport: options.allowInsecureTransport }\n : {}),\n ...(options.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: options.acceptsSensitivity }\n : {}),\n ...(options.logger !== undefined ? { logger: options.logger } : {}),\n });\n return built.provider;\n}\n"],"mappings":";;;;;;;;;;AA4DA,SAAgB,wBAAwB,SAAmD;AAkBzF,QAhBc,0BAA0B;EACtC,cAFmB,QAAQ,QAAQ,qBAAqB,QAAQ;EAGhE,OAAO,QAAQ;EACf,SAAS,QAAQ;EACjB,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,UAAU,GAAG,EAAE;EACxE,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,GAAI,QAAQ,YAAY,SAAY,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;EACrE,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,GAAI,QAAQ,2BAA2B,SACnC,EAAE,wBAAwB,QAAQ,wBAAwB,GAC1D,EAAE;EACN,GAAI,QAAQ,uBAAuB,SAC/B,EAAE,oBAAoB,QAAQ,oBAAoB,GAClD,EAAE;EACN,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EACnE,CAAC,CACW"}
1
+ {"version":3,"file":"openai-compatible.js","names":[],"sources":["../../src/adapters/openai-compatible.ts"],"sourcesContent":["/**\n * Generic OpenAI-compatible adapter - works against any HTTP server\n * that speaks the `/v1/chat/completions` REST contract. Tested\n * deployments include LMStudio (default port 1234), LocalAI (default\n * port 8080), vLLM (`python -m vllm.entrypoints.openai.api_server`,\n * default port 8000), Together-style self-host endpoints, and any\n * other server in the OpenAI-compatible ecosystem.\n *\n * The adapter shares the same `LocalProviderTrust` classifier as\n * `ollamaAdapter` and `llamaCppServerAdapter` - one classifier, one\n * policy table, one error type.\n *\n * @packageDocumentation\n */\n\nimport type { Provider, ProviderCapabilities, Sensitivity } from '@graphorin/core';\n\nimport { buildOpenAIShapedProvider } from '../internal/openai-shaped.js';\n\n/**\n * Options accepted by {@link openAICompatibleAdapter}.\n *\n * @stable\n */\nexport interface OpenAICompatibleAdapterOptions {\n /** Model identifier sent in the request body's `model` field. */\n readonly model: string;\n /**\n * Base URL of the OpenAI-compatible server. The classifier inspects\n * the protocol + host to assign a `LocalProviderTrust` value.\n */\n readonly baseUrl: string;\n /** Optional REST path override. Defaults to `/v1/chat/completions`. */\n readonly chatPath?: string;\n /** Optional bearer-auth API key. */\n readonly apiKey?: string;\n /** Extra headers merged on top of `content-type` + `accept` defaults. */\n readonly headers?: Readonly<Record<string, string>>;\n /** Custom `fetch` implementation; useful for tests. */\n readonly fetchImpl?: typeof fetch;\n /**\n * Acknowledge the risk of running over plaintext HTTP against a\n * public host.\n */\n readonly allowInsecureTransport?: boolean;\n /**\n * Capability overrides merged on top of the adapter defaults\n * (core-provider-10). Use them to widen `contextWindow` /\n * `maxOutput` for large-context servers or to set\n * `structuredOutput: false` for servers that reject\n * `response_format`.\n */\n readonly capabilities?: Partial<ProviderCapabilities>;\n /**\n * Time-to-response budget per request (PS-24). Default 120s; `0`\n * disables.\n */\n readonly timeoutMs?: number;\n /** Override for the default `acceptsSensitivity` value. */\n readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;\n /** Provider name attached to spans / log lines. */\n readonly name?: string;\n /** Optional log sink. */\n readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;\n}\n\n/**\n * Build a Graphorin {@link Provider} backed by an OpenAI-compatible\n * HTTP server. The same code path serves LMStudio, LocalAI, vLLM, and\n * any other compatible self-host endpoint.\n *\n * @stable\n */\nexport function openAICompatibleAdapter(options: OpenAICompatibleAdapterOptions): Provider {\n const providerName = options.name ?? `openai-compatible-${options.model}`;\n const built = buildOpenAIShapedProvider({\n providerName,\n model: options.model,\n baseUrl: options.baseUrl,\n ...(options.chatPath !== undefined ? { chatPath: options.chatPath } : {}),\n ...(options.apiKey !== undefined ? { apiKey: options.apiKey } : {}),\n ...(options.headers !== undefined ? { headers: options.headers } : {}),\n ...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),\n ...(options.allowInsecureTransport !== undefined\n ? { allowInsecureTransport: options.allowInsecureTransport }\n : {}),\n ...(options.capabilities !== undefined ? { capabilities: options.capabilities } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n ...(options.acceptsSensitivity !== undefined\n ? { acceptsSensitivity: options.acceptsSensitivity }\n : {}),\n ...(options.logger !== undefined ? { logger: options.logger } : {}),\n });\n return built.provider;\n}\n"],"mappings":";;;;;;;;;;AAyEA,SAAgB,wBAAwB,SAAmD;AAoBzF,QAlBc,0BAA0B;EACtC,cAFmB,QAAQ,QAAQ,qBAAqB,QAAQ;EAGhE,OAAO,QAAQ;EACf,SAAS,QAAQ;EACjB,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,UAAU,GAAG,EAAE;EACxE,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EAClE,GAAI,QAAQ,YAAY,SAAY,EAAE,SAAS,QAAQ,SAAS,GAAG,EAAE;EACrE,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,GAAI,QAAQ,2BAA2B,SACnC,EAAE,wBAAwB,QAAQ,wBAAwB,GAC1D,EAAE;EACN,GAAI,QAAQ,iBAAiB,SAAY,EAAE,cAAc,QAAQ,cAAc,GAAG,EAAE;EACpF,GAAI,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,WAAW,GAAG,EAAE;EAC3E,GAAI,QAAQ,uBAAuB,SAC/B,EAAE,oBAAoB,QAAQ,oBAAoB,GAClD,EAAE;EACN,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,QAAQ,GAAG,EAAE;EACnE,CAAC,CACW"}
@@ -0,0 +1,245 @@
1
+ //#region src/adapters/vercel-messages.ts
2
+ /**
3
+ * Registered symbols used by the AI SDK's `asSchema()` detection. They
4
+ * are created with `Symbol.for(...)` in the SDK, so a structurally
5
+ * hand-rolled wrapper is indistinguishable from the SDK's own
6
+ * `jsonSchema()` output - including across duplicated copies of the
7
+ * SDK in a node_modules tree.
8
+ */
9
+ const AI_SDK_SCHEMA_SYMBOL = Symbol.for("vercel.ai.schema");
10
+ const AI_SDK_VALIDATOR_SYMBOL = Symbol.for("vercel.ai.validator");
11
+ /**
12
+ * Wrap a plain JSON Schema record into the AI SDK `Schema` shape the
13
+ * SDK's `asSchema()` accepts. Equivalent to importing `jsonSchema`
14
+ * from `ai`, but hand-rolled so the converter works with fixture
15
+ * runtimes and does not force the optional peer at module scope.
16
+ *
17
+ * @internal
18
+ */
19
+ function wrapAsAiSdkSchema(schema) {
20
+ return {
21
+ [AI_SDK_SCHEMA_SYMBOL]: true,
22
+ [AI_SDK_VALIDATOR_SYMBOL]: true,
23
+ _type: void 0,
24
+ jsonSchema: schema,
25
+ validate: void 0
26
+ };
27
+ }
28
+ /**
29
+ * Convert Graphorin `ToolDefinition[]` to the AI SDK's name-keyed tool
30
+ * record.
31
+ *
32
+ * @internal
33
+ */
34
+ function toAiSdkTools(tools) {
35
+ const record = {};
36
+ for (const t of tools) {
37
+ const schema = wrapAsAiSdkSchema(t.inputSchema);
38
+ record[t.name] = {
39
+ ...t.description !== void 0 ? { description: t.description } : {},
40
+ inputSchema: schema,
41
+ parameters: schema
42
+ };
43
+ }
44
+ return record;
45
+ }
46
+ /**
47
+ * Convert a Graphorin `ToolChoice` to the AI SDK's spelling. The
48
+ * string modes are identical; the named-tool form differs
49
+ * (`{tool: name}` vs `{type: 'tool', toolName: name}`).
50
+ *
51
+ * @internal
52
+ */
53
+ function toAiSdkToolChoice(choice) {
54
+ if (choice === "auto" || choice === "required" || choice === "none") return choice;
55
+ return {
56
+ type: "tool",
57
+ toolName: choice.tool
58
+ };
59
+ }
60
+ /**
61
+ * Convert Graphorin messages to AI SDK `ModelMessage`s:
62
+ *
63
+ * - system-role messages are hoisted into `system` (v7 rejects them
64
+ * inside the `messages` array outright),
65
+ * - assistant `toolCalls` become `tool-call` content parts,
66
+ * - `ToolMessage`s become `{role: 'tool'}` messages carrying a
67
+ * `tool-result` part whose `toolName` is recovered from the paired
68
+ * assistant call earlier in the transcript,
69
+ * - binary content parts are renamed onto the SDK's part vocabulary
70
+ * (`file` parts carry `data`; audio has no dedicated user part and
71
+ * rides as a `file` part).
72
+ *
73
+ * @internal
74
+ */
75
+ function toAiSdkPrompt(messages) {
76
+ const out = [];
77
+ const systemParts = [];
78
+ const toolNameById = /* @__PURE__ */ new Map();
79
+ for (const msg of messages) switch (msg.role) {
80
+ case "system":
81
+ if (msg.content.length > 0) systemParts.push(msg.content);
82
+ break;
83
+ case "user":
84
+ out.push({
85
+ role: "user",
86
+ content: typeof msg.content === "string" ? msg.content : msg.content.flatMap(toUserPart)
87
+ });
88
+ break;
89
+ case "assistant": {
90
+ for (const call of msg.toolCalls ?? []) toolNameById.set(call.toolCallId, call.toolName);
91
+ if (!(msg.toolCalls !== void 0 && msg.toolCalls.length > 0) && typeof msg.content === "string") {
92
+ out.push({
93
+ role: "assistant",
94
+ content: msg.content
95
+ });
96
+ break;
97
+ }
98
+ const parts = [];
99
+ if (typeof msg.content === "string") {
100
+ if (msg.content.length > 0) parts.push({
101
+ type: "text",
102
+ text: msg.content
103
+ });
104
+ } else for (const part of msg.content) parts.push(...toAssistantPart(part));
105
+ for (const call of msg.toolCalls ?? []) parts.push({
106
+ type: "tool-call",
107
+ toolCallId: call.toolCallId,
108
+ toolName: call.toolName,
109
+ input: call.args,
110
+ args: call.args
111
+ });
112
+ out.push({
113
+ role: "assistant",
114
+ content: parts
115
+ });
116
+ break;
117
+ }
118
+ case "tool": {
119
+ const text = flattenToText(msg.content);
120
+ out.push({
121
+ role: "tool",
122
+ content: [{
123
+ type: "tool-result",
124
+ toolCallId: msg.toolCallId,
125
+ toolName: toolNameById.get(msg.toolCallId) ?? "unknown_tool",
126
+ output: {
127
+ type: "text",
128
+ value: text
129
+ },
130
+ result: text
131
+ }]
132
+ });
133
+ break;
134
+ }
135
+ }
136
+ return {
137
+ messages: out,
138
+ ...systemParts.length > 0 ? { system: systemParts.join("\n\n") } : {}
139
+ };
140
+ }
141
+ function toUserPart(part) {
142
+ switch (part.type) {
143
+ case "text": return [{
144
+ type: "text",
145
+ text: part.text
146
+ }];
147
+ case "image": return [{
148
+ type: "image",
149
+ image: part.image,
150
+ ...part.mimeType !== void 0 ? {
151
+ mediaType: part.mimeType,
152
+ mimeType: part.mimeType
153
+ } : {}
154
+ }];
155
+ case "audio": return [filePart(part.audio, part.mimeType ?? "application/octet-stream")];
156
+ case "file": return [filePart(part.file, part.mimeType, part.filename)];
157
+ case "reasoning": return [];
158
+ }
159
+ }
160
+ function toAssistantPart(part) {
161
+ switch (part.type) {
162
+ case "text": return part.text.length > 0 ? [{
163
+ type: "text",
164
+ text: part.text
165
+ }] : [];
166
+ case "reasoning": {
167
+ const meta = part.meta;
168
+ const anthropic = {};
169
+ if (meta?.provider === "anthropic") {
170
+ if (typeof meta.signature === "string") anthropic.signature = meta.signature;
171
+ if (typeof meta.data === "string") anthropic.redactedData = meta.data;
172
+ }
173
+ return [{
174
+ type: "reasoning",
175
+ text: part.text,
176
+ ...Object.keys(anthropic).length > 0 ? { providerOptions: { anthropic } } : {}
177
+ }];
178
+ }
179
+ case "image": return [filePart(part.image, part.mimeType ?? "image/png")];
180
+ case "audio": return [filePart(part.audio, part.mimeType ?? "application/octet-stream")];
181
+ case "file": return [filePart(part.file, part.mimeType, part.filename)];
182
+ }
183
+ }
184
+ function filePart(data, mimeType, filename) {
185
+ return {
186
+ type: "file",
187
+ data,
188
+ mediaType: mimeType,
189
+ mimeType,
190
+ ...filename !== void 0 ? { filename } : {}
191
+ };
192
+ }
193
+ function flattenToText(content) {
194
+ if (typeof content === "string") return content;
195
+ const buffer = [];
196
+ for (const part of content) if (part.type === "text" || part.type === "reasoning") buffer.push(part.text);
197
+ return buffer.join("");
198
+ }
199
+ /**
200
+ * Decorate the first and last converted conversation messages with an
201
+ * Anthropic `cacheControl` provider option (core-provider-02).
202
+ *
203
+ * The first-message anchor makes tools + system + the stable prefix a
204
+ * cache segment that is written once and read on every later step; the
205
+ * last-message anchor caches the whole conversation so each step's write
206
+ * becomes the next step's read. Non-Anthropic models ignore the option.
207
+ * A single-message conversation gets one anchor.
208
+ */
209
+ function applyCacheAnchors(messages, ttl) {
210
+ if (messages.length === 0) return messages;
211
+ const cacheControl = {
212
+ type: "ephemeral",
213
+ ...ttl === "1h" ? { ttl: "1h" } : {}
214
+ };
215
+ const decorate = (msg) => {
216
+ const existing = typeof msg.providerOptions === "object" && msg.providerOptions !== null ? msg.providerOptions : {};
217
+ const anthropic = typeof existing.anthropic === "object" && existing.anthropic !== null ? existing.anthropic : {};
218
+ return {
219
+ ...msg,
220
+ providerOptions: {
221
+ ...existing,
222
+ anthropic: {
223
+ ...anthropic,
224
+ cacheControl
225
+ }
226
+ }
227
+ };
228
+ };
229
+ if (messages.length === 1) {
230
+ const only = messages[0];
231
+ return only === void 0 ? messages : [decorate(only)];
232
+ }
233
+ const first = messages[0];
234
+ const last = messages[messages.length - 1];
235
+ if (first === void 0 || last === void 0) return messages;
236
+ return [
237
+ decorate(first),
238
+ ...messages.slice(1, -1),
239
+ decorate(last)
240
+ ];
241
+ }
242
+
243
+ //#endregion
244
+ export { applyCacheAnchors, toAiSdkPrompt, toAiSdkToolChoice, toAiSdkTools };
245
+ //# sourceMappingURL=vercel-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vercel-messages.js","names":["record: Record<string, unknown>","out: Array<Readonly<Record<string, unknown>>>","systemParts: string[]","parts: Array<Readonly<Record<string, unknown>>>","anthropic: Record<string, unknown>","buffer: string[]","cacheControl: Record<string, unknown>"],"sources":["../../src/adapters/vercel-messages.ts"],"sourcesContent":["/**\n * Conversion layer between Graphorin shapes and the Vercel AI SDK\n * call contract (core-provider-01).\n *\n * The AI SDK does NOT accept Graphorin messages or tool definitions\n * verbatim:\n *\n * - `tools` must be a `Record<string, Tool>` keyed by tool name whose\n * `inputSchema` is a zod / Standard Schema value or a `jsonSchema()`\n * wrapped `Schema` object. A `ToolDefinition[]` array advertises\n * tools under index keys (`\"0\"`, `\"1\"`, ...) with schemas the SDK\n * cannot consume.\n * - Assistant tool calls must be `{type: 'tool-call'}` *content parts*\n * and tool results `{role: 'tool', content: [{type: 'tool-result'}]}`\n * messages; Graphorin's separate `toolCalls` field and string-content\n * `ToolMessage` fail the SDK's `standardizePrompt` zod validation\n * (`AI_InvalidPromptError`) on the second step of any tool loop.\n *\n * Where the AI SDK renamed keys between major versions the converters\n * emit BOTH spellings (PS-6 dual-shape): `input` + `args` on tool-call\n * parts, `output` + `result` on tool-result parts, `mediaType` +\n * `mimeType` on binary parts, `inputSchema` + `parameters` on tools.\n * Both peers zod-strip the key they do not know.\n *\n * @internal\n */\n\nimport type { Message, MessageContent, ToolChoice, ToolDefinition } from '@graphorin/core';\n\n/**\n * Registered symbols used by the AI SDK's `asSchema()` detection. They\n * are created with `Symbol.for(...)` in the SDK, so a structurally\n * hand-rolled wrapper is indistinguishable from the SDK's own\n * `jsonSchema()` output - including across duplicated copies of the\n * SDK in a node_modules tree.\n */\nconst AI_SDK_SCHEMA_SYMBOL = Symbol.for('vercel.ai.schema');\nconst AI_SDK_VALIDATOR_SYMBOL = Symbol.for('vercel.ai.validator');\n\n/**\n * Wrap a plain JSON Schema record into the AI SDK `Schema` shape the\n * SDK's `asSchema()` accepts. Equivalent to importing `jsonSchema`\n * from `ai`, but hand-rolled so the converter works with fixture\n * runtimes and does not force the optional peer at module scope.\n *\n * @internal\n */\nexport function wrapAsAiSdkSchema(\n schema: Readonly<Record<string, unknown>>,\n): Readonly<Record<string, unknown>> {\n return {\n [AI_SDK_SCHEMA_SYMBOL]: true,\n [AI_SDK_VALIDATOR_SYMBOL]: true,\n _type: undefined,\n jsonSchema: schema,\n validate: undefined,\n } as Readonly<Record<string, unknown>>;\n}\n\n/**\n * Convert Graphorin `ToolDefinition[]` to the AI SDK's name-keyed tool\n * record.\n *\n * @internal\n */\nexport function toAiSdkTools(\n tools: ReadonlyArray<ToolDefinition>,\n): Readonly<Record<string, unknown>> {\n const record: Record<string, unknown> = {};\n for (const t of tools) {\n const schema = wrapAsAiSdkSchema(t.inputSchema);\n record[t.name] = {\n ...(t.description !== undefined ? { description: t.description } : {}),\n // v5+/v7 reads `inputSchema`; v4 reads `parameters` (PS-6).\n inputSchema: schema,\n parameters: schema,\n };\n }\n return record;\n}\n\n/**\n * Convert a Graphorin `ToolChoice` to the AI SDK's spelling. The\n * string modes are identical; the named-tool form differs\n * (`{tool: name}` vs `{type: 'tool', toolName: name}`).\n *\n * @internal\n */\nexport function toAiSdkToolChoice(choice: ToolChoice): unknown {\n if (choice === 'auto' || choice === 'required' || choice === 'none') return choice;\n return { type: 'tool', toolName: choice.tool };\n}\n\n/**\n * Result of {@link toAiSdkPrompt}: the converted message array plus the\n * hoisted system text (the SDK rejects `role: 'system'` entries inside\n * `messages` - system prompts ride the request-level option).\n *\n * @internal\n */\nexport interface AiSdkPrompt {\n readonly messages: ReadonlyArray<Readonly<Record<string, unknown>>>;\n readonly system?: string;\n}\n\n/**\n * Convert Graphorin messages to AI SDK `ModelMessage`s:\n *\n * - system-role messages are hoisted into `system` (v7 rejects them\n * inside the `messages` array outright),\n * - assistant `toolCalls` become `tool-call` content parts,\n * - `ToolMessage`s become `{role: 'tool'}` messages carrying a\n * `tool-result` part whose `toolName` is recovered from the paired\n * assistant call earlier in the transcript,\n * - binary content parts are renamed onto the SDK's part vocabulary\n * (`file` parts carry `data`; audio has no dedicated user part and\n * rides as a `file` part).\n *\n * @internal\n */\nexport function toAiSdkPrompt(messages: ReadonlyArray<Message>): AiSdkPrompt {\n const out: Array<Readonly<Record<string, unknown>>> = [];\n const systemParts: string[] = [];\n const toolNameById = new Map<string, string>();\n for (const msg of messages) {\n switch (msg.role) {\n case 'system':\n if (msg.content.length > 0) systemParts.push(msg.content);\n break;\n case 'user':\n out.push({\n role: 'user',\n content: typeof msg.content === 'string' ? msg.content : msg.content.flatMap(toUserPart),\n });\n break;\n case 'assistant': {\n for (const call of msg.toolCalls ?? []) {\n toolNameById.set(call.toolCallId, call.toolName);\n }\n const hasCalls = msg.toolCalls !== undefined && msg.toolCalls.length > 0;\n if (!hasCalls && typeof msg.content === 'string') {\n out.push({ role: 'assistant', content: msg.content });\n break;\n }\n const parts: Array<Readonly<Record<string, unknown>>> = [];\n if (typeof msg.content === 'string') {\n if (msg.content.length > 0) parts.push({ type: 'text', text: msg.content });\n } else {\n for (const part of msg.content) parts.push(...toAssistantPart(part));\n }\n for (const call of msg.toolCalls ?? []) {\n parts.push({\n type: 'tool-call',\n toolCallId: call.toolCallId,\n toolName: call.toolName,\n // v5+/v7 reads `input`; v4 reads `args` (PS-6).\n input: call.args,\n args: call.args,\n });\n }\n out.push({ role: 'assistant', content: parts });\n break;\n }\n case 'tool': {\n const text = flattenToText(msg.content);\n out.push({\n role: 'tool',\n content: [\n {\n type: 'tool-result',\n toolCallId: msg.toolCallId,\n toolName: toolNameById.get(msg.toolCallId) ?? 'unknown_tool',\n // v5+/v7 reads `output`; v4 reads `result` (PS-6).\n output: { type: 'text', value: text },\n result: text,\n },\n ],\n });\n break;\n }\n }\n }\n return {\n messages: out,\n ...(systemParts.length > 0 ? { system: systemParts.join('\\n\\n') } : {}),\n };\n}\n\nfunction toUserPart(part: MessageContent): ReadonlyArray<Readonly<Record<string, unknown>>> {\n switch (part.type) {\n case 'text':\n return [{ type: 'text', text: part.text }];\n case 'image':\n return [\n {\n type: 'image',\n image: part.image,\n // v5+/v7 reads `mediaType`; v4 reads `mimeType` (PS-6).\n ...(part.mimeType !== undefined\n ? { mediaType: part.mimeType, mimeType: part.mimeType }\n : {}),\n },\n ];\n case 'audio':\n return [filePart(part.audio, part.mimeType ?? 'application/octet-stream')];\n case 'file':\n return [filePart(part.file, part.mimeType, part.filename)];\n case 'reasoning':\n // Reasoning belongs to assistant turns; a stray part in user\n // content has no SDK representation. Drop it.\n return [];\n }\n}\n\nfunction toAssistantPart(part: MessageContent): ReadonlyArray<Readonly<Record<string, unknown>>> {\n switch (part.type) {\n case 'text':\n return part.text.length > 0 ? [{ type: 'text', text: part.text }] : [];\n case 'reasoning': {\n const meta = part.meta;\n const anthropic: Record<string, unknown> = {};\n if (meta?.provider === 'anthropic') {\n if (typeof meta.signature === 'string') anthropic.signature = meta.signature;\n if (typeof meta.data === 'string') anthropic.redactedData = meta.data;\n }\n return [\n {\n type: 'reasoning',\n text: part.text,\n ...(Object.keys(anthropic).length > 0 ? { providerOptions: { anthropic } } : {}),\n },\n ];\n }\n case 'image':\n return [filePart(part.image, part.mimeType ?? 'image/png')];\n case 'audio':\n return [filePart(part.audio, part.mimeType ?? 'application/octet-stream')];\n case 'file':\n return [filePart(part.file, part.mimeType, part.filename)];\n }\n}\n\nfunction filePart(\n data: Uint8Array | URL,\n mimeType: string,\n filename?: string,\n): Readonly<Record<string, unknown>> {\n return {\n type: 'file',\n data,\n // v5+/v7 reads `mediaType`; v4 reads `mimeType` (PS-6).\n mediaType: mimeType,\n mimeType,\n ...(filename !== undefined ? { filename } : {}),\n };\n}\n\nfunction flattenToText(content: string | ReadonlyArray<MessageContent>): string {\n if (typeof content === 'string') return content;\n const buffer: string[] = [];\n for (const part of content) {\n if (part.type === 'text' || part.type === 'reasoning') buffer.push(part.text);\n }\n return buffer.join('');\n}\n\n/**\n * Decorate the first and last converted conversation messages with an\n * Anthropic `cacheControl` provider option (core-provider-02).\n *\n * The first-message anchor makes tools + system + the stable prefix a\n * cache segment that is written once and read on every later step; the\n * last-message anchor caches the whole conversation so each step's write\n * becomes the next step's read. Non-Anthropic models ignore the option.\n * A single-message conversation gets one anchor.\n */\nexport function applyCacheAnchors(\n messages: ReadonlyArray<Readonly<Record<string, unknown>>>,\n ttl?: '5m' | '1h',\n): ReadonlyArray<Readonly<Record<string, unknown>>> {\n if (messages.length === 0) return messages;\n const cacheControl: Record<string, unknown> = {\n type: 'ephemeral',\n ...(ttl === '1h' ? { ttl: '1h' } : {}),\n };\n const decorate = (msg: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> => {\n const existing =\n typeof msg.providerOptions === 'object' && msg.providerOptions !== null\n ? (msg.providerOptions as Record<string, unknown>)\n : {};\n const anthropic =\n typeof existing.anthropic === 'object' && existing.anthropic !== null\n ? (existing.anthropic as Record<string, unknown>)\n : {};\n return {\n ...msg,\n providerOptions: { ...existing, anthropic: { ...anthropic, cacheControl } },\n };\n };\n if (messages.length === 1) {\n const only = messages[0];\n return only === undefined ? messages : [decorate(only)];\n }\n const first = messages[0];\n const last = messages[messages.length - 1];\n if (first === undefined || last === undefined) return messages;\n return [decorate(first), ...messages.slice(1, -1), decorate(last)];\n}\n"],"mappings":"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"}