opencode-ext-connector 0.3.3 → 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 (50) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +165 -28
  3. package/dist/core/options.d.ts +20 -0
  4. package/dist/core/options.js +66 -5
  5. package/dist/opencode/host-options.d.ts +9 -1
  6. package/dist/opencode/host-options.js +27 -7
  7. package/dist/opencode/language-factory.d.ts +1 -1
  8. package/dist/opencode/language-factory.js +3 -0
  9. package/dist/opencode/ollama-probe.d.ts +2 -1
  10. package/dist/opencode/ollama-probe.js +2 -2
  11. package/dist/opencode/ollama-production.d.ts +7 -0
  12. package/dist/opencode/ollama-production.js +19 -7
  13. package/dist/opencode/provider-entry.d.ts +1 -0
  14. package/dist/opencode/providers.d.ts +2 -0
  15. package/dist/opencode/providers.js +12 -5
  16. package/dist/opencode/v1-catalog.js +6 -1
  17. package/dist/opencode/v1-language.js +5 -2
  18. package/dist/opencode/v1-session-auth.d.ts +2 -1
  19. package/dist/opencode/v1-session-auth.js +7 -6
  20. package/dist/process/production-supervisor.d.ts +20 -0
  21. package/dist/process/production-supervisor.js +171 -0
  22. package/dist/providers/claude/credential-authority-scheduler.d.ts +23 -0
  23. package/dist/providers/claude/credential-authority-scheduler.js +181 -0
  24. package/dist/providers/command-code/language-model.js +46 -39
  25. package/dist/providers/command-code/open-generation.d.ts +16 -0
  26. package/dist/providers/command-code/open-generation.js +45 -0
  27. package/dist/providers/cursor/credential-retry.d.ts +14 -0
  28. package/dist/providers/cursor/credential-retry.js +26 -0
  29. package/dist/providers/cursor/direct-run-failure.d.ts +8 -0
  30. package/dist/providers/cursor/direct-run-failure.js +26 -0
  31. package/dist/providers/cursor/direct-run-types.d.ts +1 -0
  32. package/dist/providers/cursor/direct-run.js +40 -27
  33. package/dist/providers/cursor/direct-runtime.js +26 -39
  34. package/dist/providers/cursor/run-session.js +1 -1
  35. package/dist/providers/ollama/adapter.d.ts +2 -0
  36. package/dist/providers/ollama/adapter.js +3 -1
  37. package/dist/providers/ollama/endpoints.d.ts +8 -0
  38. package/dist/providers/ollama/endpoints.js +38 -0
  39. package/dist/providers/ollama/index.d.ts +1 -0
  40. package/dist/providers/ollama/index.js +1 -0
  41. package/dist/providers/ollama/language-model.d.ts +2 -0
  42. package/dist/providers/ollama/language-model.js +2 -0
  43. package/dist/providers/ollama/local-catalog.d.ts +2 -1
  44. package/dist/providers/ollama/local-catalog.js +3 -3
  45. package/dist/providers/ollama/runtime.d.ts +2 -0
  46. package/dist/providers/ollama/runtime.js +5 -5
  47. package/dist/sdk/ollama.d.ts +4 -1
  48. package/dist/server.js +55 -13
  49. package/docs/README.ko.md +165 -28
  50. package/package.json +1 -1
@@ -2,32 +2,32 @@
2
2
  // Licensed under MIT. See THIRD_PARTY_NOTICES.md.
3
3
  import { AdapterError, OperationCancelledError } from "../../core/errors.js";
4
4
  import { parseProviderId } from "../../core/ids.js";
5
- import { openHttpBody } from "../../http/read-body.js";
6
5
  import { createCommandCodeVersionResolver } from "./cli-version.js";
7
6
  import { emitCommandCodeChunks } from "./emit-stream.js";
8
7
  import { commandCodeMissingBodyError } from "./errors.js";
8
+ import { openCommandCodeGeneration } from "./open-generation.js";
9
9
  import { buildBody, buildHeaders, } from "./request.js";
10
- import { commandCodeHttpError, createCommandCodeRequestLifecycle, readCommandCodeErrorBody, } from "./request-lifecycle.js";
10
+ import { createCommandCodeRequestLifecycle } from "./request-lifecycle.js";
11
11
  import { createCommandCodeSessionId } from "./session.js";
12
- function buildRequestOptions(options, call, token, cliVersion) {
13
- const bodyOptions = {
14
- modelId: options.modelId,
15
- call,
16
- sessionId: options.sessionId,
17
- };
12
+ function buildRequestOptions(options) {
13
+ const { runtime, call, token, cliVersion, bodySnapshot } = options;
18
14
  const headerOptions = {
19
15
  token,
20
16
  cliVersion,
21
- sessionId: options.sessionId,
17
+ sessionId: runtime.sessionId,
22
18
  };
19
+ const headers = {};
20
+ for (const source of [buildHeaders(headerOptions), runtime.headers ?? {}, call.headers ?? {}]) {
21
+ for (const [name, value] of Object.entries(source)) {
22
+ if (value !== undefined) {
23
+ headers[name.toLowerCase()] = value;
24
+ }
25
+ }
26
+ }
23
27
  return {
24
- url: `${(options.baseURL ?? "https://api.commandcode.ai").replace(/\/+$/, "")}/alpha/generate`,
25
- headers: {
26
- ...buildHeaders(headerOptions),
27
- ...options.headers,
28
- ...Object.fromEntries(Object.entries(call.headers ?? {}).filter((entry) => entry[1] !== undefined)),
29
- },
30
- body: new TextEncoder().encode(JSON.stringify(buildBody(bodyOptions))),
28
+ url: `${(runtime.baseURL ?? "https://api.commandcode.ai").replace(/\/+$/, "")}/alpha/generate`,
29
+ headers,
30
+ body: new Uint8Array(bodySnapshot),
31
31
  };
32
32
  }
33
33
  function createUsage() {
@@ -66,34 +66,41 @@ async function streamCommandCode(options, call) {
66
66
  providerId: parseProviderId("command-code"),
67
67
  });
68
68
  }
69
- const requestOptions = buildRequestOptions(options, call, token, cliVersion);
70
- let opened;
69
+ const bodyOptions = {
70
+ modelId: options.modelId,
71
+ call,
72
+ sessionId: options.sessionId,
73
+ };
74
+ const bodySnapshot = new TextEncoder().encode(JSON.stringify(buildBody(bodyOptions)));
75
+ let generation;
71
76
  try {
72
- opened = await openHttpBody(options.transport, {
73
- method: "POST",
74
- url: requestOptions.url,
75
- headers: requestOptions.headers,
76
- body: requestOptions.body,
77
- }, lifecycle.signal);
77
+ generation = await openCommandCodeGeneration({
78
+ transport: options.transport,
79
+ lifecycle,
80
+ initialToken: token,
81
+ readAccessToken: options.readAccessToken,
82
+ createRequest: (accessToken) => {
83
+ const request = buildRequestOptions({
84
+ runtime: options,
85
+ call,
86
+ token: accessToken,
87
+ cliVersion,
88
+ bodySnapshot,
89
+ });
90
+ return {
91
+ method: "POST",
92
+ url: request.url,
93
+ headers: request.headers,
94
+ body: request.body,
95
+ };
96
+ },
97
+ });
78
98
  }
79
99
  catch (error) {
80
100
  lifecycle.dispose();
81
101
  throw error;
82
102
  }
83
- if (opened.status < 200 || opened.status >= 300) {
84
- let errorBody;
85
- try {
86
- errorBody = await readCommandCodeErrorBody(opened.chunks);
87
- }
88
- catch (error) {
89
- lifecycle.abort();
90
- throw error;
91
- }
92
- finally {
93
- lifecycle.dispose();
94
- }
95
- throw commandCodeHttpError(opened.status, errorBody);
96
- }
103
+ const { opened, request } = generation;
97
104
  if (!opened.bodyPresent) {
98
105
  lifecycle.dispose();
99
106
  throw commandCodeMissingBodyError(opened.status);
@@ -124,7 +131,7 @@ async function streamCommandCode(options, call) {
124
131
  });
125
132
  return {
126
133
  stream,
127
- request: { body: new TextDecoder().decode(requestOptions.body) },
134
+ request: { body: request.body === null ? undefined : new TextDecoder().decode(request.body) },
128
135
  response: { headers: opened.headers },
129
136
  };
130
137
  }
@@ -0,0 +1,16 @@
1
+ import type { HttpRequest, HttpTransport } from "../../core/http.js";
2
+ import { type HttpBodyStream } from "../../http/read-body.js";
3
+ import type { CommandCodeRequestLifecycle } from "./request-lifecycle.js";
4
+ type OpenGenerationOptions = {
5
+ readonly transport: HttpTransport;
6
+ readonly lifecycle: CommandCodeRequestLifecycle;
7
+ readonly initialToken: string;
8
+ readonly readAccessToken: (signal: AbortSignal) => Promise<string | null>;
9
+ readonly createRequest: (token: string) => HttpRequest;
10
+ };
11
+ export type OpenedCommandCodeGeneration = {
12
+ readonly opened: HttpBodyStream;
13
+ readonly request: HttpRequest;
14
+ };
15
+ export declare function openCommandCodeGeneration(options: OpenGenerationOptions): Promise<OpenedCommandCodeGeneration>;
16
+ export {};
@@ -0,0 +1,45 @@
1
+ // Derived from brent-weatherall/opencode-commandcode-provider src/model.ts.
2
+ // Licensed under MIT. See THIRD_PARTY_NOTICES.md.
3
+ import { OperationCancelledError } from "../../core/errors.js";
4
+ import { openHttpBody } from "../../http/read-body.js";
5
+ import { commandCodeHttpError, readCommandCodeErrorBody } from "./request-lifecycle.js";
6
+ async function openAttempt(options, request) {
7
+ const opened = await openHttpBody(options.transport, request, options.lifecycle.signal);
8
+ return { opened, request };
9
+ }
10
+ async function responseError(opened, lifecycle) {
11
+ try {
12
+ const body = await readCommandCodeErrorBody(opened.chunks);
13
+ return commandCodeHttpError(opened.status, body);
14
+ }
15
+ catch (error) {
16
+ lifecycle.abort();
17
+ throw error;
18
+ }
19
+ }
20
+ function successful(opened) {
21
+ return opened.status >= 200 && opened.status < 300;
22
+ }
23
+ export async function openCommandCodeGeneration(options) {
24
+ const firstRequest = options.createRequest(options.initialToken);
25
+ const first = await openAttempt(options, firstRequest);
26
+ if (successful(first.opened))
27
+ return first;
28
+ const firstError = await responseError(first.opened, options.lifecycle);
29
+ if (first.opened.status !== 401)
30
+ throw firstError;
31
+ const reloadedToken = await options.readAccessToken(options.lifecycle.signal);
32
+ if (options.lifecycle.signal.aborted) {
33
+ throw new OperationCancelledError("command-code-stream");
34
+ }
35
+ if (reloadedToken === null)
36
+ throw firstError;
37
+ const candidateRequest = options.createRequest(reloadedToken);
38
+ if (candidateRequest.headers["authorization"] === firstRequest.headers["authorization"]) {
39
+ throw firstError;
40
+ }
41
+ const second = await openAttempt(options, candidateRequest);
42
+ if (successful(second.opened))
43
+ return second;
44
+ throw await responseError(second.opened, options.lifecycle);
45
+ }
@@ -0,0 +1,14 @@
1
+ import type { CursorReplayState } from "./recovery.js";
2
+ export declare function isCursorCredentialRetryEligible(input: {
3
+ readonly error: unknown;
4
+ readonly replay: CursorReplayState;
5
+ readonly retryUsed: boolean;
6
+ }): boolean;
7
+ export type CursorCredentialRetry = {
8
+ readonly currentToken: () => string;
9
+ readonly reload: (error: unknown, replay: CursorReplayState, signal: AbortSignal) => Promise<boolean>;
10
+ };
11
+ export declare function createCursorCredentialRetry(options: {
12
+ readonly initialToken: string;
13
+ readonly reloadAccessToken: (signal: AbortSignal) => Promise<string | null>;
14
+ }): CursorCredentialRetry;
@@ -0,0 +1,26 @@
1
+ import { CursorDirectStreamError } from "./direct-stream.js";
2
+ export function isCursorCredentialRetryEligible(input) {
3
+ return (!input.retryUsed &&
4
+ input.error instanceof CursorDirectStreamError &&
5
+ input.error.bridgeCode === "http-401" &&
6
+ input.replay.outputEpoch === 0 &&
7
+ input.replay.checkpointEpoch === null &&
8
+ !input.replay.toolBoundary);
9
+ }
10
+ export function createCursorCredentialRetry(options) {
11
+ let currentToken = options.initialToken;
12
+ let retryUsed = false;
13
+ return {
14
+ currentToken: () => currentToken,
15
+ reload: async (error, replay, signal) => {
16
+ if (!isCursorCredentialRetryEligible({ error, replay, retryUsed }))
17
+ return false;
18
+ retryUsed = true;
19
+ const reloadedToken = await options.reloadAccessToken(signal);
20
+ if (reloadedToken === null || reloadedToken === currentToken)
21
+ return false;
22
+ currentToken = reloadedToken;
23
+ return true;
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,8 @@
1
+ import type { CursorDirectSetupCleanup } from "./direct-run-types.js";
2
+ import type { CursorRunSession } from "./run-session.js";
3
+ import type { CursorStreamAdapter } from "./stream-adapter.js";
4
+ export declare function retireCursorSessionForRetry(session: CursorRunSession, cause: unknown): Promise<void>;
5
+ export declare function failCursorDirectRun(logical: {
6
+ readonly adapter: CursorStreamAdapter;
7
+ readonly cleanup: CursorDirectSetupCleanup;
8
+ }, session: CursorRunSession | null, error: unknown): Promise<void>;
@@ -0,0 +1,26 @@
1
+ export async function retireCursorSessionForRetry(session, cause) {
2
+ try {
3
+ const retireForRetry = session.retireForRetry;
4
+ if (retireForRetry === undefined)
5
+ throw new TypeError("retry retirement is unavailable");
6
+ await retireForRetry();
7
+ }
8
+ catch (retirementError) {
9
+ throw new AggregateError([cause, retirementError], "Cursor retry retirement failed");
10
+ }
11
+ }
12
+ export async function failCursorDirectRun(logical, session, error) {
13
+ try {
14
+ if (session === null) {
15
+ logical.cleanup.invalidateCheckpoint();
16
+ logical.cleanup.invalidateSession();
17
+ }
18
+ else
19
+ await session.abort();
20
+ }
21
+ catch (cleanupError) {
22
+ logical.adapter.fail(new AggregateError([error, cleanupError], "Cursor recovery cleanup failed"));
23
+ return;
24
+ }
25
+ logical.adapter.fail(error);
26
+ }
@@ -25,6 +25,7 @@ export type CursorDirectRunOptions = {
25
25
  readonly createSetupCleanup?: (resources: CursorDirectSetupCleanupResources) => CursorDirectSetupCleanup;
26
26
  readonly idleTimeoutMs: number;
27
27
  readonly modelId: string;
28
+ readonly reloadAccessToken: (signal: AbortSignal) => Promise<string | null>;
28
29
  readonly registry: CursorRunSessionRegistry;
29
30
  readonly signal: AbortSignal;
30
31
  readonly token: string;
@@ -3,6 +3,8 @@ import { parseModelId } from "../../core/ids.js";
3
3
  import { createCursorBlobStore } from "./blob-store.js";
4
4
  import { createCursorCheckpointStore } from "./checkpoint-store.js";
5
5
  import { encodeConnectFrame } from "./connect-frame.js";
6
+ import { createCursorCredentialRetry } from "./credential-retry.js";
7
+ import { failCursorDirectRun, retireCursorSessionForRetry } from "./direct-run-failure.js";
6
8
  import { consumeCursorDirectAttempt, isCursorRetryableStreamError } from "./direct-stream.js";
7
9
  import { cursorMcpDefinitions } from "./exec-reply.js";
8
10
  import { cursorPromptText } from "./prompt.js";
@@ -76,13 +78,13 @@ function buildRequest(options, logical, mode) {
76
78
  },
77
79
  });
78
80
  }
79
- async function openAttempt(options, logical, mode) {
80
- const request = buildRequest(options, logical, mode);
81
+ async function openAttempt(options, logical, attempt) {
82
+ const request = buildRequest(options, logical, attempt.mode);
81
83
  let stream = null;
82
84
  try {
83
85
  stream = await options.bridge.open({
84
86
  id: options.createId(),
85
- accessToken: options.token,
87
+ accessToken: attempt.token,
86
88
  path: CURSOR_RUN_PATH,
87
89
  headers: { "content-type": "application/connect+proto", "connect-protocol-version": "1" },
88
90
  signal: options.signal,
@@ -123,27 +125,19 @@ async function openAttempt(options, logical, mode) {
123
125
  throw error;
124
126
  }
125
127
  }
126
- async function failRun(logical, session, error) {
127
- try {
128
- if (session === null) {
129
- logical.cleanup.invalidateCheckpoint();
130
- logical.cleanup.invalidateSession();
131
- }
132
- else
133
- await session.abort();
134
- }
135
- catch (cleanupError) {
136
- logical.adapter.fail(new AggregateError([error, cleanupError], "Cursor recovery cleanup failed"));
137
- return;
138
- }
139
- logical.adapter.fail(error);
140
- }
141
128
  export async function startCursorDirectRun(options) {
142
129
  const logical = createLogicalRun(options);
143
130
  const planner = createCursorRecoveryPlanner();
131
+ const credential = createCursorCredentialRetry({
132
+ initialToken: options.token,
133
+ reloadAccessToken: options.reloadAccessToken,
134
+ });
144
135
  let session;
145
136
  try {
146
- session = await openAttempt(options, logical, "initial");
137
+ session = await openAttempt(options, logical, {
138
+ mode: "initial",
139
+ token: credential.currentToken(),
140
+ });
147
141
  }
148
142
  catch (error) {
149
143
  logical.cleanup.releaseOwnership();
@@ -177,7 +171,25 @@ export async function startCursorDirectRun(options) {
177
171
  const idle = watchdog.expired();
178
172
  watchdog.dispose();
179
173
  if (options.signal.aborted) {
180
- await failRun(logical, session, new OperationCancelledError("cursor-direct-stream"));
174
+ await failCursorDirectRun(logical, session, new OperationCancelledError("cursor-direct-stream"));
175
+ return;
176
+ }
177
+ try {
178
+ const changed = await credential.reload(error, logical.adapter.replayState(), options.signal);
179
+ if (options.signal.aborted)
180
+ throw new OperationCancelledError("cursor-direct-stream");
181
+ if (changed) {
182
+ await retireCursorSessionForRetry(session, error);
183
+ session = null;
184
+ session = await openAttempt(options, logical, {
185
+ mode: "initial",
186
+ token: credential.currentToken(),
187
+ });
188
+ continue;
189
+ }
190
+ }
191
+ catch (retryError) {
192
+ await failCursorDirectRun(logical, session, retryError);
181
193
  return;
182
194
  }
183
195
  const decision = planner.next({
@@ -190,20 +202,21 @@ export async function startCursorDirectRun(options) {
190
202
  planner.requireRetry(decision, error);
191
203
  }
192
204
  catch (recoveryError) {
193
- await failRun(logical, session, recoveryError);
205
+ await failCursorDirectRun(logical, session, recoveryError);
194
206
  }
195
207
  return;
196
208
  }
197
209
  try {
198
- const retireForRetry = session.retireForRetry;
199
- if (retireForRetry === undefined)
200
- throw new TypeError("retry retirement is unavailable");
201
- await retireForRetry();
210
+ await retireCursorSessionForRetry(session, error);
211
+ session = null;
202
212
  logical.adapter.suspendForRetry();
203
- session = await openAttempt(options, logical, decision.mode);
213
+ session = await openAttempt(options, logical, {
214
+ mode: decision.mode,
215
+ token: credential.currentToken(),
216
+ });
204
217
  }
205
218
  catch (retryError) {
206
- await failRun(logical, session, retryError);
219
+ await failCursorDirectRun(logical, session, retryError);
207
220
  return;
208
221
  }
209
222
  }
@@ -41,7 +41,9 @@ export function createCursorDirectRuntime(options) {
41
41
  return bridgeClient;
42
42
  };
43
43
  const doStream = async (call, modelId) => {
44
- const signal = call.abortSignal ?? new AbortController().signal;
44
+ const signal = call.abortSignal === undefined
45
+ ? lifecycle.signal
46
+ : AbortSignal.any([lifecycle.signal, call.abortSignal]);
45
47
  if (signal.aborted)
46
48
  throw new OperationCancelledError("cursor-direct-stream");
47
49
  const resultCallIds = [];
@@ -59,46 +61,31 @@ export function createCursorDirectRuntime(options) {
59
61
  await existing.writeContinuations(continuations, signal);
60
62
  return { stream: await consumeCursorDirectSession({ session: existing, signal, registry }) };
61
63
  }
62
- const setup = new AbortController();
63
- const cancel = () => {
64
- if (!setup.signal.aborted) {
65
- setup.abort(new OperationCancelledError("cursor-direct-stream"));
66
- }
67
- };
68
- lifecycle.signal.addEventListener("abort", cancel, { once: true });
69
- call.abortSignal?.addEventListener("abort", cancel, { once: true });
70
- if (lifecycle.signal.aborted || signal.aborted)
71
- cancel();
72
- try {
73
- const token = await options.readAccessToken(setup.signal);
74
- if (setup.signal.aborted)
75
- throw new OperationCancelledError("cursor-direct-stream");
76
- if (token === null)
77
- throw cursorFailure("cursor-auth-unavailable");
78
- return await startCursorDirectRun({
79
- bridge: await bridge(),
80
- call,
81
- clock: options.clock,
82
- createId,
83
- idleTimeoutMs: options.idleTimeoutMs ?? 60_000,
84
- modelId,
85
- registry,
86
- signal,
87
- token,
88
- tools: toolsFromCall(call),
89
- ttlMs: options.ttlMs ?? 300_000,
90
- ...(options.createSetupCleanup === undefined
91
- ? {}
92
- : { createSetupCleanup: options.createSetupCleanup }),
93
- });
94
- }
95
- finally {
96
- lifecycle.signal.removeEventListener("abort", cancel);
97
- call.abortSignal?.removeEventListener("abort", cancel);
98
- }
64
+ const token = await options.readAccessToken(signal);
65
+ if (signal.aborted)
66
+ throw new OperationCancelledError("cursor-direct-stream");
67
+ if (token === null)
68
+ throw cursorFailure("cursor-auth-unavailable");
69
+ return await startCursorDirectRun({
70
+ bridge: await bridge(),
71
+ call,
72
+ clock: options.clock,
73
+ createId,
74
+ idleTimeoutMs: options.idleTimeoutMs ?? 60_000,
75
+ modelId,
76
+ reloadAccessToken: options.readAccessToken,
77
+ registry,
78
+ signal,
79
+ token,
80
+ tools: toolsFromCall(call),
81
+ ttlMs: options.ttlMs ?? 300_000,
82
+ ...(options.createSetupCleanup === undefined
83
+ ? {}
84
+ : { createSetupCleanup: options.createSetupCleanup }),
85
+ });
99
86
  };
100
87
  const disposal = createAsyncDisposable(async () => {
101
- lifecycle.abort();
88
+ lifecycle.abort(new OperationCancelledError("cursor-direct-stream"));
102
89
  await settleCursorCleanup([
103
90
  registry.dispose,
104
91
  async () => {
@@ -150,12 +150,12 @@ export function createCursorRunSessionRegistry(_options) {
150
150
  retireForRetry: async () => {
151
151
  if (resources.dispatcher.parkedCalls.size > 0 || reserved.size > 0)
152
152
  throw new CursorRunSessionError("retry-boundary");
153
- retired = true;
154
153
  cancelTimer();
155
154
  sessions.delete(identity.sessionId);
156
155
  removeOwnership(identity.sessionId);
157
156
  resources.dispatcher.parkedCalls.clear();
158
157
  await settleCursorCleanup([resources.stream.abort, resources.ownership.release]);
158
+ retired = true;
159
159
  },
160
160
  dispose: disposal.dispose,
161
161
  };
@@ -1,8 +1,10 @@
1
1
  import type { ProviderAdapter } from "../../core/adapter.js";
2
2
  import type { OllamaCatalogState } from "./catalog-state.js";
3
+ import { type OllamaEndpoints } from "./endpoints.js";
3
4
  import type { OllamaFetch } from "./http.js";
4
5
  export type OllamaAdapterOptions = {
5
6
  readonly fetch: OllamaFetch;
6
7
  readonly catalog: OllamaCatalogState;
8
+ readonly endpoints?: OllamaEndpoints;
7
9
  };
8
10
  export declare function createOllamaAdapter(options: OllamaAdapterOptions): ProviderAdapter;
@@ -1,6 +1,7 @@
1
1
  import { OperationCancelledError } from "../../core/errors.js";
2
2
  import { parseProviderId } from "../../core/ids.js";
3
3
  import { createAsyncDisposable } from "../../core/lifecycle.js";
4
+ import { parseOllamaEndpoints } from "./endpoints.js";
4
5
  import { OllamaCatalogError } from "./errors.js";
5
6
  import { listLocalOllamaModels } from "./local-catalog.js";
6
7
  function mergeModels(local, cloud) {
@@ -23,6 +24,7 @@ function catalogFailure(error) {
23
24
  }
24
25
  export function createOllamaAdapter(options) {
25
26
  const providerId = parseProviderId("ollama");
27
+ const endpoints = options.endpoints ?? parseOllamaEndpoints(undefined);
26
28
  const lease = options.catalog.acquire();
27
29
  const disposal = createAsyncDisposable(() => lease.dispose());
28
30
  let lastMergedModels = null;
@@ -33,7 +35,7 @@ export function createOllamaAdapter(options) {
33
35
  throw new OperationCancelledError("ollama-snapshot");
34
36
  let local;
35
37
  try {
36
- local = await listLocalOllamaModels(options.fetch, signal);
38
+ local = await listLocalOllamaModels(options.fetch, signal, endpoints);
37
39
  }
38
40
  catch (error) {
39
41
  const failure = catalogFailure(error);
@@ -0,0 +1,8 @@
1
+ export declare const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
2
+ export type OllamaEndpoints = {
3
+ readonly baseURL: string;
4
+ readonly tagsURL: string;
5
+ readonly pullURL: string;
6
+ readonly chatURL: string;
7
+ };
8
+ export declare function parseOllamaEndpoints(input: unknown): OllamaEndpoints;
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ export const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
3
+ const DirectApiPaths = ["/api/tags", "/api/pull", "/api/chat"];
4
+ const OllamaBaseURLSchema = z
5
+ .string()
6
+ .refine((value) => value.length > 0 &&
7
+ value === value.trim() &&
8
+ /^https?:\/\//iu.test(value) &&
9
+ !/[\t\r\n\\]/u.test(value) &&
10
+ !/^https?:\/\/[^/?#]*@/iu.test(value) &&
11
+ !/[?#]$/u.test(value) &&
12
+ URL.canParse(value))
13
+ .transform((value) => new URL(value))
14
+ .superRefine((url, context) => {
15
+ const cloudHostname = url.hostname.toLowerCase().replace(/\.+$/u, "");
16
+ const path = url.pathname.replace(/\/+$/u, "") || "/";
17
+ if ((url.protocol !== "http:" && url.protocol !== "https:") ||
18
+ url.hostname.length === 0 ||
19
+ url.username.length > 0 ||
20
+ url.password.length > 0 ||
21
+ url.search.length > 0 ||
22
+ url.hash.length > 0 ||
23
+ cloudHostname === "ollama.com" ||
24
+ cloudHostname.endsWith(".ollama.com") ||
25
+ DirectApiPaths.some((apiPath) => path.endsWith(apiPath))) {
26
+ context.addIssue({ code: "custom", message: "invalid Ollama daemon base URL" });
27
+ }
28
+ })
29
+ .transform((url) => `${url.origin}${url.pathname.replace(/\/+$/u, "")}`);
30
+ export function parseOllamaEndpoints(input) {
31
+ const baseURL = OllamaBaseURLSchema.parse(input === undefined ? DEFAULT_OLLAMA_BASE_URL : input);
32
+ return Object.freeze({
33
+ baseURL,
34
+ tagsURL: `${baseURL}/api/tags`,
35
+ pullURL: `${baseURL}/api/pull`,
36
+ chatURL: `${baseURL}/api/chat`,
37
+ });
38
+ }
@@ -1,6 +1,7 @@
1
1
  export { createOllamaAdapter, type OllamaAdapterOptions } from "./adapter.js";
2
2
  export { createOllamaCatalogState, type OllamaCatalogLease, type OllamaCatalogState, type OllamaCatalogStateOptions, } from "./catalog-state.js";
3
3
  export { discoverOllamaCloudModels } from "./cloud-catalog.js";
4
+ export { DEFAULT_OLLAMA_BASE_URL, type OllamaEndpoints, parseOllamaEndpoints, } from "./endpoints.js";
4
5
  export { OllamaCatalogError, type OllamaCatalogErrorKind, OllamaGenerationError, type OllamaGenerationOperation, } from "./errors.js";
5
6
  export { type OllamaFetch, productionOllamaFetch } from "./http.js";
6
7
  export { createOllamaLanguageModel, type OllamaLanguageModelOptions, } from "./language-model.js";
@@ -1,6 +1,7 @@
1
1
  export { createOllamaAdapter } from "./adapter.js";
2
2
  export { createOllamaCatalogState, } from "./catalog-state.js";
3
3
  export { discoverOllamaCloudModels } from "./cloud-catalog.js";
4
+ export { DEFAULT_OLLAMA_BASE_URL, parseOllamaEndpoints, } from "./endpoints.js";
4
5
  export { OllamaCatalogError, OllamaGenerationError, } from "./errors.js";
5
6
  export { productionOllamaFetch } from "./http.js";
6
7
  export { createOllamaLanguageModel, } from "./language-model.js";
@@ -1,5 +1,6 @@
1
1
  import type { LanguageModelV3 } from "@ai-sdk/provider";
2
2
  import type { OllamaCatalogState } from "./catalog-state.js";
3
+ import { type OllamaEndpoints } from "./endpoints.js";
3
4
  import type { OllamaFetch } from "./http.js";
4
5
  import { type OllamaRuntime } from "./runtime.js";
5
6
  export type OllamaLanguageModelOptions = {
@@ -7,5 +8,6 @@ export type OllamaLanguageModelOptions = {
7
8
  readonly runtime?: OllamaRuntime;
8
9
  readonly catalog?: OllamaCatalogState;
9
10
  readonly fetch?: OllamaFetch;
11
+ readonly endpoints?: OllamaEndpoints;
10
12
  };
11
13
  export declare function createOllamaLanguageModel(options: OllamaLanguageModelOptions): LanguageModelV3;
@@ -1,4 +1,5 @@
1
1
  import { OperationCancelledError } from "../../core/errors.js";
2
+ import { parseOllamaEndpoints } from "./endpoints.js";
2
3
  import { generateFromOllamaStream } from "./generate.js";
3
4
  import { buildOllamaCall } from "./prompt.js";
4
5
  import { createOllamaRuntime } from "./runtime.js";
@@ -10,6 +11,7 @@ function runtimeFromOptions(options) {
10
11
  throw new TypeError("Ollama catalog state is required");
11
12
  return createOllamaRuntime({
12
13
  catalog: options.catalog,
14
+ endpoints: options.endpoints ?? parseOllamaEndpoints(undefined),
13
15
  ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
14
16
  });
15
17
  }
@@ -1,3 +1,4 @@
1
1
  import type { AdapterModel } from "../../core/models.js";
2
+ import { type OllamaEndpoints } from "./endpoints.js";
2
3
  import { type OllamaFetch } from "./http.js";
3
- export declare function listLocalOllamaModels(fetch: OllamaFetch, signal: AbortSignal): Promise<readonly AdapterModel[]>;
4
+ export declare function listLocalOllamaModels(fetch: OllamaFetch, signal: AbortSignal, endpoints?: OllamaEndpoints): Promise<readonly AdapterModel[]>;
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { parseAdapterModel } from "../../core/models.js";
3
+ import { parseOllamaEndpoints } from "./endpoints.js";
3
4
  import { OllamaCatalogError } from "./errors.js";
4
5
  import { requestOllamaCatalog } from "./http.js";
5
- const LOCAL_TAGS_URL = "http://localhost:11434/api/tags";
6
6
  const LocalModelSchema = z
7
7
  .object({ model: z.string().optional(), name: z.string().optional() })
8
8
  .superRefine((value, context) => {
@@ -17,9 +17,9 @@ const LocalModelSchema = z
17
17
  })
18
18
  .transform(({ model, name }) => (model?.length === 0 ? undefined : model) ?? name ?? "");
19
19
  const LocalTagsSchema = z.object({ models: z.array(LocalModelSchema) }).readonly();
20
- export async function listLocalOllamaModels(fetch, signal) {
20
+ export async function listLocalOllamaModels(fetch, signal, endpoints = parseOllamaEndpoints(undefined)) {
21
21
  const text = await requestOllamaCatalog({
22
- url: LOCAL_TAGS_URL,
22
+ url: endpoints.tagsURL,
23
23
  accept: "application/json",
24
24
  operation: "local-tags",
25
25
  fetch,