@graphorin/provider 0.5.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 (148) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +105 -0
  4. package/dist/adapters/index.d.ts +4 -0
  5. package/dist/adapters/index.js +6 -0
  6. package/dist/adapters/llamacpp-server.d.ts +69 -0
  7. package/dist/adapters/llamacpp-server.d.ts.map +1 -0
  8. package/dist/adapters/llamacpp-server.js +51 -0
  9. package/dist/adapters/llamacpp-server.js.map +1 -0
  10. package/dist/adapters/ollama.d.ts +43 -0
  11. package/dist/adapters/ollama.d.ts.map +1 -0
  12. package/dist/adapters/ollama.js +246 -0
  13. package/dist/adapters/ollama.js.map +1 -0
  14. package/dist/adapters/openai-compatible.d.ts +48 -0
  15. package/dist/adapters/openai-compatible.d.ts.map +1 -0
  16. package/dist/adapters/openai-compatible.js +28 -0
  17. package/dist/adapters/openai-compatible.js.map +1 -0
  18. package/dist/adapters/vercel.d.ts +135 -0
  19. package/dist/adapters/vercel.d.ts.map +1 -0
  20. package/dist/adapters/vercel.js +306 -0
  21. package/dist/adapters/vercel.js.map +1 -0
  22. package/dist/counters/anthropic.d.ts +36 -0
  23. package/dist/counters/anthropic.d.ts.map +1 -0
  24. package/dist/counters/anthropic.js +63 -0
  25. package/dist/counters/anthropic.js.map +1 -0
  26. package/dist/counters/bedrock.d.ts +27 -0
  27. package/dist/counters/bedrock.d.ts.map +1 -0
  28. package/dist/counters/bedrock.js +29 -0
  29. package/dist/counters/bedrock.js.map +1 -0
  30. package/dist/counters/dispatcher.d.ts +34 -0
  31. package/dist/counters/dispatcher.d.ts.map +1 -0
  32. package/dist/counters/dispatcher.js +78 -0
  33. package/dist/counters/dispatcher.js.map +1 -0
  34. package/dist/counters/global.d.ts +26 -0
  35. package/dist/counters/global.d.ts.map +1 -0
  36. package/dist/counters/global.js +31 -0
  37. package/dist/counters/global.js.map +1 -0
  38. package/dist/counters/google.d.ts +27 -0
  39. package/dist/counters/google.d.ts.map +1 -0
  40. package/dist/counters/google.js +29 -0
  41. package/dist/counters/google.js.map +1 -0
  42. package/dist/counters/heuristic.d.ts +37 -0
  43. package/dist/counters/heuristic.d.ts.map +1 -0
  44. package/dist/counters/heuristic.js +64 -0
  45. package/dist/counters/heuristic.js.map +1 -0
  46. package/dist/counters/index.d.ts +9 -0
  47. package/dist/counters/index.js +10 -0
  48. package/dist/counters/js-tiktoken.d.ts +63 -0
  49. package/dist/counters/js-tiktoken.d.ts.map +1 -0
  50. package/dist/counters/js-tiktoken.js +73 -0
  51. package/dist/counters/js-tiktoken.js.map +1 -0
  52. package/dist/counters/serialize.d.ts +37 -0
  53. package/dist/counters/serialize.d.ts.map +1 -0
  54. package/dist/counters/serialize.js +57 -0
  55. package/dist/counters/serialize.js.map +1 -0
  56. package/dist/errors/errors.d.ts +167 -0
  57. package/dist/errors/errors.d.ts.map +1 -0
  58. package/dist/errors/errors.js +182 -0
  59. package/dist/errors/errors.js.map +1 -0
  60. package/dist/errors/index.d.ts +2 -0
  61. package/dist/errors/index.js +3 -0
  62. package/dist/index.d.ts +69 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +70 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/internal/abort.js +34 -0
  67. package/dist/internal/abort.js.map +1 -0
  68. package/dist/internal/http.js +163 -0
  69. package/dist/internal/http.js.map +1 -0
  70. package/dist/internal/openai-shaped.js +308 -0
  71. package/dist/internal/openai-shaped.js.map +1 -0
  72. package/dist/internal/sse.js +104 -0
  73. package/dist/internal/sse.js.map +1 -0
  74. package/dist/internal/url-utils.js +24 -0
  75. package/dist/internal/url-utils.js.map +1 -0
  76. package/dist/middleware/compose.d.ts +85 -0
  77. package/dist/middleware/compose.d.ts.map +1 -0
  78. package/dist/middleware/compose.js +174 -0
  79. package/dist/middleware/compose.js.map +1 -0
  80. package/dist/middleware/index.d.ts +10 -0
  81. package/dist/middleware/index.js +11 -0
  82. package/dist/middleware/production-hook.d.ts +27 -0
  83. package/dist/middleware/production-hook.d.ts.map +1 -0
  84. package/dist/middleware/production-hook.js +21 -0
  85. package/dist/middleware/production-hook.js.map +1 -0
  86. package/dist/middleware/with-cost-limit.d.ts +46 -0
  87. package/dist/middleware/with-cost-limit.d.ts.map +1 -0
  88. package/dist/middleware/with-cost-limit.js +66 -0
  89. package/dist/middleware/with-cost-limit.js.map +1 -0
  90. package/dist/middleware/with-cost-tracking.d.ts +91 -0
  91. package/dist/middleware/with-cost-tracking.d.ts.map +1 -0
  92. package/dist/middleware/with-cost-tracking.js +95 -0
  93. package/dist/middleware/with-cost-tracking.js.map +1 -0
  94. package/dist/middleware/with-fallback.d.ts +25 -0
  95. package/dist/middleware/with-fallback.d.ts.map +1 -0
  96. package/dist/middleware/with-fallback.js +84 -0
  97. package/dist/middleware/with-fallback.js.map +1 -0
  98. package/dist/middleware/with-rate-limit.d.ts +36 -0
  99. package/dist/middleware/with-rate-limit.d.ts.map +1 -0
  100. package/dist/middleware/with-rate-limit.js +141 -0
  101. package/dist/middleware/with-rate-limit.js.map +1 -0
  102. package/dist/middleware/with-redaction.d.ts +60 -0
  103. package/dist/middleware/with-redaction.d.ts.map +1 -0
  104. package/dist/middleware/with-redaction.js +305 -0
  105. package/dist/middleware/with-redaction.js.map +1 -0
  106. package/dist/middleware/with-retry.d.ts +26 -0
  107. package/dist/middleware/with-retry.d.ts.map +1 -0
  108. package/dist/middleware/with-retry.js +157 -0
  109. package/dist/middleware/with-retry.js.map +1 -0
  110. package/dist/middleware/with-tracing.d.ts +21 -0
  111. package/dist/middleware/with-tracing.d.ts.map +1 -0
  112. package/dist/middleware/with-tracing.js +55 -0
  113. package/dist/middleware/with-tracing.js.map +1 -0
  114. package/dist/model-tier/classify.d.ts +34 -0
  115. package/dist/model-tier/classify.d.ts.map +1 -0
  116. package/dist/model-tier/classify.js +117 -0
  117. package/dist/model-tier/classify.js.map +1 -0
  118. package/dist/model-tier/index.d.ts +2 -0
  119. package/dist/model-tier/index.js +3 -0
  120. package/dist/provider.d.ts +51 -0
  121. package/dist/provider.d.ts.map +1 -0
  122. package/dist/provider.js +64 -0
  123. package/dist/provider.js.map +1 -0
  124. package/dist/reasoning/apply-policy.d.ts +22 -0
  125. package/dist/reasoning/apply-policy.d.ts.map +1 -0
  126. package/dist/reasoning/apply-policy.js +47 -0
  127. package/dist/reasoning/apply-policy.js.map +1 -0
  128. package/dist/reasoning/classify-contract.d.ts +41 -0
  129. package/dist/reasoning/classify-contract.d.ts.map +1 -0
  130. package/dist/reasoning/classify-contract.js +59 -0
  131. package/dist/reasoning/classify-contract.js.map +1 -0
  132. package/dist/reasoning/index.d.ts +4 -0
  133. package/dist/reasoning/index.js +5 -0
  134. package/dist/reasoning/retention.d.ts +39 -0
  135. package/dist/reasoning/retention.d.ts.map +1 -0
  136. package/dist/reasoning/retention.js +33 -0
  137. package/dist/reasoning/retention.js.map +1 -0
  138. package/dist/tool-examples.d.ts +14 -0
  139. package/dist/tool-examples.d.ts.map +1 -0
  140. package/dist/tool-examples.js +35 -0
  141. package/dist/tool-examples.js.map +1 -0
  142. package/dist/trust/classify-local-provider.d.ts +47 -0
  143. package/dist/trust/classify-local-provider.d.ts.map +1 -0
  144. package/dist/trust/classify-local-provider.js +164 -0
  145. package/dist/trust/classify-local-provider.js.map +1 -0
  146. package/dist/trust/index.d.ts +3 -0
  147. package/dist/trust/index.js +3 -0
  148. package/package.json +128 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # @graphorin/provider
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release. See the root `CHANGELOG.md` and the corresponding
6
+ changeset for the full list of features.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksiy Stepurenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # `@graphorin/provider`
2
+
3
+ > Vendor-neutral LLM provider layer for the
4
+ > [**Graphorin**](https://github.com/o-stepper/graphorin) framework.
5
+
6
+ The package owns four moving parts:
7
+
8
+ 1. **`createProvider(...)`** — wraps any adapter in a stable `Provider`
9
+ shape with sensitivity, capability, and reasoning-retention defaults.
10
+ 2. **Adapters** — `vercelAdapter` (default cloud path; wraps the Vercel
11
+ AI SDK), `ollamaAdapter` (direct Ollama HTTP), `llamaCppServerAdapter`
12
+ (the upstream `llama-server` binary from llama.cpp), and
13
+ `openAICompatibleAdapter` (LMStudio / LocalAI / vLLM / Together-style
14
+ self-host endpoints).
15
+ 3. **Middleware** — `composeProviderMiddleware([...])` enforces a
16
+ canonical order at startup and throws `MiddlewareOrderingError` on
17
+ violation. Built-ins: `withTracing`, `withRetry`, `withRateLimit`,
18
+ `withCostLimit`, `withCostTracking`, `withFallback`, and
19
+ `withRedaction` (mandatory in production).
20
+ 4. **Token counting** — pluggable `TokenCounter` dispatcher. Default
21
+ `JsTiktokenCounter` for OpenAI-compatible models; per-vendor native
22
+ counters for Anthropic, Google, and Bedrock; heuristic fallback for
23
+ unknown providers with a one-time WARN.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pnpm add @graphorin/provider
29
+ # Optional peer for the cloud adapter:
30
+ pnpm add ai
31
+ # Optional peer for the default token counter:
32
+ pnpm add js-tiktoken
33
+ ```
34
+
35
+ For the in-process llama.cpp companion adapter, install the separate
36
+ package:
37
+
38
+ ```bash
39
+ pnpm add @graphorin/provider-llamacpp-node node-llama-cpp
40
+ ```
41
+
42
+ ## Quick start
43
+
44
+ ```ts
45
+ import { composeProviderMiddleware, createProvider } from '@graphorin/provider';
46
+ import { vercelAdapter } from '@graphorin/provider/adapters/vercel';
47
+ import {
48
+ withCostLimit,
49
+ withFallback,
50
+ withRateLimit,
51
+ withRedaction,
52
+ withRetry,
53
+ withTracing,
54
+ } from '@graphorin/provider/middleware';
55
+ import { BUILT_IN_PATTERNS } from '@graphorin/observability/redaction/patterns';
56
+ import { openai } from '@ai-sdk/openai';
57
+
58
+ const provider = createProvider(vercelAdapter(openai('gpt-4o')));
59
+
60
+ const safeProvider = composeProviderMiddleware([
61
+ withTracing(),
62
+ withRetry({ maxRetries: 3 }),
63
+ withRateLimit({ requestsPerMinute: 60 }),
64
+ withCostLimit({ maxPerSession: 1.0 }),
65
+ withFallback([fallbackProvider]),
66
+ withRedaction({ patterns: BUILT_IN_PATTERNS }), // INNERMOST
67
+ ])(provider);
68
+ ```
69
+
70
+ ## Local-first stack
71
+
72
+ ```ts
73
+ import { ollamaAdapter } from '@graphorin/provider/adapters/ollama';
74
+
75
+ // Auto-classified as 'loopback' — no warning, no first-run prompt.
76
+ const local = createProvider(
77
+ ollamaAdapter({ model: 'llama3.1:8b', baseUrl: 'http://127.0.0.1:11434' }),
78
+ );
79
+ ```
80
+
81
+ The same `LocalProviderTrust` classifier (`'loopback' | 'private' |
82
+ 'public-tls' | 'public-cleartext'`) drives the trust auto-detection,
83
+ the sensitivity-tier defaults, and the `withRedaction` policy table
84
+ for every `baseUrl`-driven adapter — `ollamaAdapter`,
85
+ `llamaCppServerAdapter`, and `openAICompatibleAdapter`. The classifier
86
+ lives at `@graphorin/provider/trust`. Public-cleartext URLs refuse to
87
+ start with `LocalProviderInsecureTransportError`.
88
+
89
+ ## Cost-tier vocabulary
90
+
91
+ ```ts
92
+ import type { ModelHint } from '@graphorin/core';
93
+ import { classifyModelTier } from '@graphorin/provider/model-tier';
94
+
95
+ classifyModelTier(provider); // ⇒ 'fast' | 'balanced' | 'smart' | undefined
96
+ ```
97
+
98
+ The classifier is consumed by the agent runtime (Phase 12) to validate
99
+ operator-supplied per-tier mappings and to surface tier-not-mapped
100
+ recommendations.
101
+
102
+ ## Project metadata
103
+
104
+ - **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko
105
+ - Repository: <https://github.com/o-stepper/graphorin>
@@ -0,0 +1,4 @@
1
+ import { LlamaCppServerAdapterOptions, llamaCppServerAdapter } from "./llamacpp-server.js";
2
+ import { OllamaAdapterOptions, ollamaAdapter } from "./ollama.js";
3
+ import { OpenAICompatibleAdapterOptions, openAICompatibleAdapter } from "./openai-compatible.js";
4
+ import { VercelAdapterOptions, vercelAdapter } from "./vercel.js";
@@ -0,0 +1,6 @@
1
+ import { llamaCppServerAdapter } from "./llamacpp-server.js";
2
+ import { ollamaAdapter } from "./ollama.js";
3
+ import { openAICompatibleAdapter } from "./openai-compatible.js";
4
+ import { vercelAdapter } from "./vercel.js";
5
+
6
+ export { };
@@ -0,0 +1,69 @@
1
+ import * as _graphorin_core0 from "@graphorin/core";
2
+ import { Provider, Sensitivity } from "@graphorin/core";
3
+
4
+ //#region src/adapters/llamacpp-server.d.ts
5
+
6
+ /**
7
+ * Default port used by the upstream `llama-server` binary.
8
+ *
9
+ * @stable
10
+ */
11
+ declare const DEFAULT_LLAMACPP_SERVER_BASE_URL = "http://127.0.0.1:8080";
12
+ /**
13
+ * Options accepted by {@link llamaCppServerAdapter}.
14
+ *
15
+ * @stable
16
+ */
17
+ interface LlamaCppServerAdapterOptions {
18
+ /** GGUF model identifier exposed by the running server (e.g. `'qwen2.5:7b-instruct-q4_k_m'`). */
19
+ readonly model: string;
20
+ /** Base URL of the running `llama-server` process. Defaults to `http://127.0.0.1:8080`. */
21
+ readonly baseUrl?: string;
22
+ /** Optional bearer-auth API key (`--api-key` flag on the server). */
23
+ readonly apiKey?: string;
24
+ /** Extra headers merged on top of `content-type` + `accept` defaults. */
25
+ readonly headers?: Readonly<Record<string, string>>;
26
+ /** Custom `fetch` implementation; useful for tests. */
27
+ readonly fetchImpl?: typeof fetch;
28
+ /**
29
+ * Time-to-response budget per request (PS-24). Default
30
+ * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.
31
+ */
32
+ readonly timeoutMs?: number;
33
+ /** Capability overrides merged on top of the adapter defaults. */
34
+ readonly capabilities?: Partial<_graphorin_core0.ProviderCapabilities>;
35
+ /**
36
+ * Acknowledge the risk of running over plaintext HTTP against a
37
+ * public host. Without this flag the adapter throws
38
+ * {@link import('../errors/errors.js').LocalProviderInsecureTransportError}.
39
+ */
40
+ readonly allowInsecureTransport?: boolean;
41
+ /** Override for the default `acceptsSensitivity` value. */
42
+ readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;
43
+ /** Provider name attached to spans / log lines. */
44
+ readonly name?: string;
45
+ /** Optional log sink. Tests pass a fixture sink to silence the console. */
46
+ readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;
47
+ }
48
+ /**
49
+ * Build a Graphorin {@link Provider} backed by the upstream
50
+ * `llama-server` binary. The factory does not start the binary —
51
+ * operators launch it themselves with the desired model + GPU flags
52
+ * and pass the URL here.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const local = createProvider(
57
+ * llamaCppServerAdapter({
58
+ * model: 'qwen2.5:7b-instruct-q4_k_m',
59
+ * baseUrl: 'http://127.0.0.1:8080',
60
+ * }),
61
+ * );
62
+ * ```
63
+ *
64
+ * @stable
65
+ */
66
+ declare function llamaCppServerAdapter(options: LlamaCppServerAdapterOptions): Provider;
67
+ //#endregion
68
+ export { DEFAULT_LLAMACPP_SERVER_BASE_URL, LlamaCppServerAdapterOptions, llamaCppServerAdapter };
69
+ //# sourceMappingURL=llamacpp-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llamacpp-server.d.ts","names":[],"sources":["../../src/adapters/llamacpp-server.ts"],"sourcesContent":[],"mappings":";;;;;AAgFA;;;;;cAzDa,gCAAA;;;;;;UAOI,4BAAA;;;;;;;;qBAQI,SAAS;;8BAEA;;;;;;;0BAOJ,QAPS,gBAAA,CAOyB,oBAAA;;;;;;;;gCAQ5B,cAAc;;;;;;;;;;;;;;;;;;;;;;;;iBAyB9B,qBAAA,UAA+B,+BAA+B"}
@@ -0,0 +1,51 @@
1
+ import { buildOpenAIShapedProvider } from "../internal/openai-shaped.js";
2
+
3
+ //#region src/adapters/llamacpp-server.ts
4
+ /**
5
+ * Default port used by the upstream `llama-server` binary.
6
+ *
7
+ * @stable
8
+ */
9
+ const DEFAULT_LLAMACPP_SERVER_BASE_URL = "http://127.0.0.1:8080";
10
+ /**
11
+ * Build a Graphorin {@link Provider} backed by the upstream
12
+ * `llama-server` binary. The factory does not start the binary —
13
+ * operators launch it themselves with the desired model + GPU flags
14
+ * and pass the URL here.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const local = createProvider(
19
+ * llamaCppServerAdapter({
20
+ * model: 'qwen2.5:7b-instruct-q4_k_m',
21
+ * baseUrl: 'http://127.0.0.1:8080',
22
+ * }),
23
+ * );
24
+ * ```
25
+ *
26
+ * @stable
27
+ */
28
+ function llamaCppServerAdapter(options) {
29
+ const baseUrl = options.baseUrl ?? DEFAULT_LLAMACPP_SERVER_BASE_URL;
30
+ return buildOpenAIShapedProvider({
31
+ providerName: options.name ?? `llamacpp-server-${options.model}`,
32
+ model: options.model,
33
+ baseUrl,
34
+ ...options.apiKey !== void 0 ? { apiKey: options.apiKey } : {},
35
+ ...options.headers !== void 0 ? { headers: options.headers } : {},
36
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
37
+ ...options.allowInsecureTransport !== void 0 ? { allowInsecureTransport: options.allowInsecureTransport } : {},
38
+ ...options.acceptsSensitivity !== void 0 ? { acceptsSensitivity: options.acceptsSensitivity } : {},
39
+ ...options.logger !== void 0 ? { logger: options.logger } : {},
40
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
41
+ capabilities: {
42
+ multimodal: false,
43
+ parallelToolCalls: false,
44
+ ...options.capabilities
45
+ }
46
+ }).provider;
47
+ }
48
+
49
+ //#endregion
50
+ export { DEFAULT_LLAMACPP_SERVER_BASE_URL, llamaCppServerAdapter };
51
+ //# sourceMappingURL=llamacpp-server.js.map
@@ -0,0 +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"}
@@ -0,0 +1,43 @@
1
+ import { Provider, ProviderCapabilities, Sensitivity } from "@graphorin/core";
2
+
3
+ //#region src/adapters/ollama.d.ts
4
+
5
+ /**
6
+ * Default Ollama base URL.
7
+ *
8
+ * @stable
9
+ */
10
+ declare const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
11
+ /**
12
+ * Options accepted by {@link ollamaAdapter}.
13
+ *
14
+ * @stable
15
+ */
16
+ interface OllamaAdapterOptions {
17
+ readonly model: string;
18
+ readonly baseUrl?: string;
19
+ readonly chatPath?: string;
20
+ readonly headers?: Readonly<Record<string, string>>;
21
+ readonly fetchImpl?: typeof fetch;
22
+ /**
23
+ * Time-to-response budget per request (PS-24). Default
24
+ * `DEFAULT_REQUEST_TIMEOUT_MS` (120s); `0` disables.
25
+ */
26
+ readonly timeoutMs?: number;
27
+ readonly allowInsecureTransport?: boolean;
28
+ readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;
29
+ readonly capabilities?: Partial<ProviderCapabilities>;
30
+ readonly name?: string;
31
+ readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;
32
+ }
33
+ /**
34
+ * Build a Graphorin {@link Provider} backed by Ollama's native HTTP
35
+ * API. The adapter is fail-safe by default: public-cleartext URLs
36
+ * refuse to start with `LocalProviderInsecureTransportError`.
37
+ *
38
+ * @stable
39
+ */
40
+ declare function ollamaAdapter(options: OllamaAdapterOptions): Provider;
41
+ //#endregion
42
+ export { DEFAULT_OLLAMA_BASE_URL, OllamaAdapterOptions, ollamaAdapter };
43
+ //# sourceMappingURL=ollama.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,246 @@
1
+ import { LocalProviderInsecureTransportError, ProviderStreamParseError } from "../errors/errors.js";
2
+ import { applyReasoningPolicy } from "../reasoning/apply-policy.js";
3
+ import { resolveReasoningRetention } from "../reasoning/retention.js";
4
+ import { classifyLocalProvider } from "../trust/classify-local-provider.js";
5
+ import { callJsonHttp, makeStreamStartEvent, toOllamaChatMessages } from "../internal/http.js";
6
+ import { parseNdJsonStream } from "../internal/sse.js";
7
+ import { stripTrailingSlashes } from "../internal/url-utils.js";
8
+ import { randomUUID } from "node:crypto";
9
+
10
+ //#region src/adapters/ollama.ts
11
+ /**
12
+ * Direct adapter for the Ollama HTTP API. The adapter speaks the
13
+ * native Ollama streaming JSON protocol (`POST /api/chat` returning
14
+ * newline-delimited JSON objects). For operators who prefer the
15
+ * OpenAI-compatible variant exposed by recent Ollama releases, the
16
+ * generic {@link openAICompatibleAdapter} is the better choice — both
17
+ * adapters share the same {@link LocalProviderTrust} classifier and
18
+ * {@link LocalProviderInsecureTransportError} startup behaviour.
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ /**
23
+ * Default Ollama base URL.
24
+ *
25
+ * @stable
26
+ */
27
+ const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
28
+ const DEFAULT_CAPABILITIES = {
29
+ streaming: true,
30
+ toolCalling: true,
31
+ parallelToolCalls: false,
32
+ multimodal: false,
33
+ structuredOutput: true,
34
+ reasoning: false,
35
+ contextWindow: 8192,
36
+ maxOutput: 4096,
37
+ reasoningContract: "optional"
38
+ };
39
+ /**
40
+ * Build a Graphorin {@link Provider} backed by Ollama's native HTTP
41
+ * API. The adapter is fail-safe by default: public-cleartext URLs
42
+ * refuse to start with `LocalProviderInsecureTransportError`.
43
+ *
44
+ * @stable
45
+ */
46
+ function ollamaAdapter(options) {
47
+ const baseUrl = options.baseUrl ?? DEFAULT_OLLAMA_BASE_URL;
48
+ const classification = classifyLocalProvider(baseUrl);
49
+ if (classification.trust === "public-cleartext" && options.allowInsecureTransport !== true) throw new LocalProviderInsecureTransportError(baseUrl);
50
+ emitTrustWarning(options.logger ?? defaultLogger, classification, baseUrl);
51
+ const acceptsSensitivity = options.acceptsSensitivity ?? classification.acceptsSensitivity;
52
+ const providerName = options.name ?? `ollama-${options.model}`;
53
+ const capabilities = {
54
+ ...DEFAULT_CAPABILITIES,
55
+ ...options.capabilities
56
+ };
57
+ const chatPath = options.chatPath ?? "/api/chat";
58
+ const url = `${stripTrailingSlashes(baseUrl)}${chatPath}`;
59
+ return {
60
+ name: providerName,
61
+ modelId: options.model,
62
+ capabilities,
63
+ acceptsSensitivity,
64
+ stream(req) {
65
+ return streamOllama(options, providerName, url, applyOllamaPreflight(req, capabilities));
66
+ },
67
+ async generate(req) {
68
+ return generateOllama(options, providerName, url, applyOllamaPreflight(req, capabilities));
69
+ }
70
+ };
71
+ }
72
+ function applyOllamaPreflight(req, capabilities) {
73
+ const retention = resolveReasoningRetention({
74
+ ...req.reasoningRetention !== void 0 ? { requested: req.reasoningRetention } : {},
75
+ ...capabilities.reasoningContract !== void 0 ? { contract: capabilities.reasoningContract } : {}
76
+ });
77
+ if (retention === "pass-through-all") return req;
78
+ const filtered = applyReasoningPolicy({
79
+ messages: req.messages,
80
+ retention
81
+ });
82
+ if (filtered === req.messages) return req;
83
+ return {
84
+ ...req,
85
+ messages: filtered
86
+ };
87
+ }
88
+ async function* streamOllama(options, providerName, url, req) {
89
+ const body = buildBody(options.model, req, true, options.capabilities?.structuredOutput ?? true);
90
+ const resp = await callJsonHttp({
91
+ providerName,
92
+ url,
93
+ headers: buildHeaders(options),
94
+ body,
95
+ ...req.signal !== void 0 ? { signal: req.signal } : {},
96
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
97
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
98
+ });
99
+ yield makeStreamStartEvent({
100
+ providerName,
101
+ modelId: options.model
102
+ });
103
+ let usage = {
104
+ promptTokens: 0,
105
+ completionTokens: 0,
106
+ totalTokens: 0
107
+ };
108
+ let finishReason = "stop";
109
+ for await (const line of parseNdJsonStream(resp.body, req.signal !== void 0 ? { signal: req.signal } : {})) {
110
+ if (req.signal?.aborted) {
111
+ finishReason = "aborted";
112
+ break;
113
+ }
114
+ let chunk;
115
+ try {
116
+ chunk = JSON.parse(line);
117
+ } catch (cause) {
118
+ throw new ProviderStreamParseError(providerName, `failed to parse ndjson line: ${cause.message}`, cause);
119
+ }
120
+ if (chunk.message?.content !== void 0 && chunk.message.content.length > 0) yield {
121
+ type: "text-delta",
122
+ delta: chunk.message.content
123
+ };
124
+ if (Array.isArray(chunk.message?.tool_calls)) for (const tc of chunk.message.tool_calls) {
125
+ const toolCallId = tc.id ?? `call_${randomUUID()}`;
126
+ const toolName = tc.function?.name ?? "";
127
+ if (toolName.length === 0) continue;
128
+ yield {
129
+ type: "tool-call-start",
130
+ toolCallId,
131
+ toolName
132
+ };
133
+ yield {
134
+ type: "tool-call-end",
135
+ toolCallId,
136
+ finalArgs: tc.function?.arguments ?? {}
137
+ };
138
+ }
139
+ if (chunk.done === true) {
140
+ finishReason = mapFinishReason(chunk.done_reason);
141
+ usage = mapUsage(chunk);
142
+ break;
143
+ }
144
+ }
145
+ yield {
146
+ type: "finish",
147
+ finishReason,
148
+ usage
149
+ };
150
+ }
151
+ async function generateOllama(options, providerName, url, req) {
152
+ const body = buildBody(options.model, req, false, options.capabilities?.structuredOutput ?? true);
153
+ const resp = await callJsonHttp({
154
+ providerName,
155
+ url,
156
+ headers: buildHeaders(options),
157
+ body,
158
+ ...req.signal !== void 0 ? { signal: req.signal } : {},
159
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
160
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
161
+ });
162
+ let json;
163
+ try {
164
+ json = await resp.json();
165
+ } catch (cause) {
166
+ throw new ProviderStreamParseError(providerName, "response body was not valid JSON", cause);
167
+ }
168
+ return {
169
+ usage: mapUsage(json),
170
+ finishReason: mapFinishReason(json.done_reason),
171
+ ...typeof json.message?.content === "string" && json.message.content.length > 0 ? { text: json.message.content } : {},
172
+ ...Array.isArray(json.message?.tool_calls) && json.message.tool_calls.length > 0 ? { toolCalls: json.message.tool_calls.map((tc) => ({
173
+ toolCallId: tc.id ?? `call_${randomUUID()}`,
174
+ toolName: tc.function?.name ?? "",
175
+ args: tc.function?.arguments ?? {}
176
+ })) } : {}
177
+ };
178
+ }
179
+ function buildBody(model, req, stream, structuredOutput) {
180
+ const body = {
181
+ model,
182
+ messages: toOllamaChatMessages(req.systemMessage !== void 0 ? [{
183
+ role: "system",
184
+ content: req.systemMessage
185
+ }, ...req.messages] : req.messages),
186
+ stream
187
+ };
188
+ if (req.temperature !== void 0 || req.maxTokens !== void 0) {
189
+ const optionsBlock = {};
190
+ if (req.temperature !== void 0) optionsBlock.temperature = req.temperature;
191
+ if (req.maxTokens !== void 0) optionsBlock.num_predict = req.maxTokens;
192
+ body.options = optionsBlock;
193
+ }
194
+ if (req.tools !== void 0 && req.tools.length > 0) body.tools = req.tools.map((t) => ({
195
+ type: "function",
196
+ function: {
197
+ name: t.name,
198
+ ...t.description !== void 0 ? { description: t.description } : {},
199
+ parameters: t.inputSchema
200
+ }
201
+ }));
202
+ if (structuredOutput && req.outputType?.kind === "structured") body.format = req.outputType.jsonSchema ?? "json";
203
+ if (req.providerOptions !== void 0) Object.assign(body, req.providerOptions);
204
+ return body;
205
+ }
206
+ function buildHeaders(options) {
207
+ return {
208
+ "content-type": "application/json",
209
+ accept: "application/json",
210
+ ...options.headers
211
+ };
212
+ }
213
+ function mapFinishReason(value) {
214
+ switch (value) {
215
+ case "stop":
216
+ case "length": return value;
217
+ case "tool_calls": return "tool-calls";
218
+ default: return "stop";
219
+ }
220
+ }
221
+ function mapUsage(chunk) {
222
+ const promptTokens = chunk.prompt_eval_count ?? 0;
223
+ const completionTokens = chunk.eval_count ?? 0;
224
+ return {
225
+ promptTokens,
226
+ completionTokens,
227
+ totalTokens: promptTokens + completionTokens
228
+ };
229
+ }
230
+ function emitTrustWarning(log, classification, baseUrl) {
231
+ if (classification.trust === "public-cleartext") log("warn", `[ollama] allowInsecureTransport=true accepted for ${baseUrl}`, { baseUrl });
232
+ else if (classification.trust === "public-tls") log("warn", `[ollama] public-TLS endpoint; treating as cloud-tier`, { baseUrl });
233
+ else if (classification.trust === "private") log("warn", `[ollama] private-network endpoint detected (${classification.reason})`, {
234
+ baseUrl,
235
+ acceptsSensitivity: classification.acceptsSensitivity
236
+ });
237
+ }
238
+ function defaultLogger(level, message, meta) {
239
+ const fn = level === "warn" ? console.warn : console.info;
240
+ if (meta !== void 0) fn(`[graphorin/provider] ${message}`, meta);
241
+ else fn(`[graphorin/provider] ${message}`);
242
+ }
243
+
244
+ //#endregion
245
+ export { DEFAULT_OLLAMA_BASE_URL, ollamaAdapter };
246
+ //# sourceMappingURL=ollama.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,48 @@
1
+ import { Provider, Sensitivity } from "@graphorin/core";
2
+
3
+ //#region src/adapters/openai-compatible.d.ts
4
+
5
+ /**
6
+ * Options accepted by {@link openAICompatibleAdapter}.
7
+ *
8
+ * @stable
9
+ */
10
+ interface OpenAICompatibleAdapterOptions {
11
+ /** Model identifier sent in the request body's `model` field. */
12
+ readonly model: string;
13
+ /**
14
+ * Base URL of the OpenAI-compatible server. The classifier inspects
15
+ * the protocol + host to assign a `LocalProviderTrust` value.
16
+ */
17
+ readonly baseUrl: string;
18
+ /** Optional REST path override. Defaults to `/v1/chat/completions`. */
19
+ readonly chatPath?: string;
20
+ /** Optional bearer-auth API key. */
21
+ readonly apiKey?: string;
22
+ /** Extra headers merged on top of `content-type` + `accept` defaults. */
23
+ readonly headers?: Readonly<Record<string, string>>;
24
+ /** Custom `fetch` implementation; useful for tests. */
25
+ readonly fetchImpl?: typeof fetch;
26
+ /**
27
+ * Acknowledge the risk of running over plaintext HTTP against a
28
+ * public host.
29
+ */
30
+ readonly allowInsecureTransport?: boolean;
31
+ /** Override for the default `acceptsSensitivity` value. */
32
+ readonly acceptsSensitivity?: ReadonlyArray<Sensitivity>;
33
+ /** Provider name attached to spans / log lines. */
34
+ readonly name?: string;
35
+ /** Optional log sink. */
36
+ readonly logger?: (level: 'warn' | 'info', message: string, meta?: object) => void;
37
+ }
38
+ /**
39
+ * Build a Graphorin {@link Provider} backed by an OpenAI-compatible
40
+ * HTTP server. The same code path serves LMStudio, LocalAI, vLLM, and
41
+ * any other compatible self-host endpoint.
42
+ *
43
+ * @stable
44
+ */
45
+ declare function openAICompatibleAdapter(options: OpenAICompatibleAdapterOptions): Provider;
46
+ //#endregion
47
+ export { OpenAICompatibleAdapterOptions, openAICompatibleAdapter };
48
+ //# sourceMappingURL=openai-compatible.d.ts.map