@love-moon/ai-sdk 0.2.30 → 0.2.31

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.
package/dist/client.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- export function createAiSession(backend: any, options?: {}): import("./session-factory.js").CodexAppServerSession | import("./session-factory.js").ClaudeAgentSdkSession | import("./session-factory.js").KimiCliSession | import("./session-factory.js").OpencodeSdkSession | RemoteAiSession;
1
+ export function createAiSession(backend: any, options?: {}): RemoteAiSession | LocalAiSessionProxy;
2
2
  export class RemoteAiSession extends EventEmitter<[never]> {
3
3
  constructor(backend: any, options?: {});
4
- backend: string;
5
4
  options: {};
6
5
  logger: any;
7
- variant: string;
8
6
  threadIdValue: any;
9
7
  threadOptionsValue: {
10
8
  model: any;
@@ -12,8 +10,8 @@ export class RemoteAiSession extends EventEmitter<[never]> {
12
10
  useSessionFileReplyStreamValue: boolean;
13
11
  sessionInfo: any;
14
12
  snapshot: {
15
- backend: string;
16
- provider: string;
13
+ backend: undefined;
14
+ provider: undefined;
17
15
  sessionId: any;
18
16
  useSessionFileReplyStream: boolean;
19
17
  workerReady: boolean;
@@ -31,14 +29,18 @@ export class RemoteAiSession extends EventEmitter<[never]> {
31
29
  resolveReady: ((value: any) => void) | null;
32
30
  rejectReady: ((reason?: any) => void) | null;
33
31
  readyPromise: Promise<any>;
32
+ initializeSession(backend: any, options: any): Promise<void>;
33
+ backend: any;
34
+ variant: any;
34
35
  get threadId(): any;
35
36
  get threadOptions(): {
37
+ modelProvider?: any;
36
38
  model: any;
37
39
  };
38
40
  getSnapshot(): {
39
41
  sessionInfo: any;
40
- backend: string;
41
- provider: string;
42
+ backend: undefined;
43
+ provider: undefined;
42
44
  sessionId: any;
43
45
  useSessionFileReplyStream: boolean;
44
46
  workerReady: boolean;
@@ -63,5 +65,43 @@ export class RemoteAiSession extends EventEmitter<[never]> {
63
65
  handleWorkerEvent(payload: any): Promise<void>;
64
66
  rejectPendingRequests(error: any): void;
65
67
  }
68
+ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
69
+ constructor(backend: any, options?: {});
70
+ backend: any;
71
+ options: {};
72
+ threadIdValue: any;
73
+ threadOptionsValue: {
74
+ model: any;
75
+ };
76
+ useSessionFileReplyStreamValue: boolean;
77
+ sessionInfo: any;
78
+ session: any;
79
+ closed: boolean;
80
+ sessionMessageHandler: any;
81
+ workingStatusHandler: any;
82
+ replyTarget: any;
83
+ snapshot: {
84
+ backend: undefined;
85
+ provider: undefined;
86
+ sessionId: any;
87
+ useSessionFileReplyStream: boolean;
88
+ workerReady: boolean;
89
+ };
90
+ readyPromise: Promise<any>;
91
+ initializeSession(backend: any, options: any): Promise<any>;
92
+ get threadId(): any;
93
+ get threadOptions(): any;
94
+ getSnapshot(): any;
95
+ usesSessionFileReplyStream(): boolean;
96
+ getSessionInfo(): any;
97
+ setSessionMessageHandler(handler: any): void;
98
+ setWorkingStatusHandler(handler: any): void;
99
+ setSessionReplyTarget(replyTarget: any): void;
100
+ ensureSessionInfo(): Promise<any>;
101
+ getSessionUsageSummary(): Promise<any>;
102
+ runTurn(promptText: any, options?: {}): Promise<any>;
103
+ close(): Promise<void>;
104
+ }
66
105
  import { EventEmitter } from "node:events";
67
106
  import readline from "node:readline";
107
+ export {};
package/dist/client.js CHANGED
@@ -26,10 +26,8 @@ function toSerializablePayload(payload) {
26
26
  export class RemoteAiSession extends EventEmitter {
27
27
  constructor(backend, options = {}) {
28
28
  super();
29
- this.backend = assertSupportedBackend(backend);
30
29
  this.options = options;
31
30
  this.logger = normalizeLogger(options.logger);
32
- this.variant = providerVariantForBackend(this.backend);
33
31
  this.threadIdValue =
34
32
  typeof options.resumeSessionId === "string" && options.resumeSessionId.trim()
35
33
  ? options.resumeSessionId.trim()
@@ -37,13 +35,13 @@ export class RemoteAiSession extends EventEmitter {
37
35
  this.threadOptionsValue = {
38
36
  model: typeof options.model === "string" && options.model.trim()
39
37
  ? options.model.trim()
40
- : this.backend || "unknown",
38
+ : String(backend || "unknown").trim() || "unknown",
41
39
  };
42
40
  this.useSessionFileReplyStreamValue = true;
43
41
  this.sessionInfo = null;
44
42
  this.snapshot = {
45
- backend: this.backend,
46
- provider: this.variant,
43
+ backend: undefined,
44
+ provider: undefined,
47
45
  sessionId: this.threadIdValue || undefined,
48
46
  useSessionFileReplyStream: this.useSessionFileReplyStreamValue,
49
47
  workerReady: false,
@@ -94,17 +92,45 @@ export class RemoteAiSession extends EventEmitter {
94
92
  this.rejectReady?.(error);
95
93
  this.rejectPendingRequests(error);
96
94
  });
97
- this.child.stdin.write(toSerializablePayload({
98
- type: "create",
99
- backend: this.backend,
100
- options: sanitizeOptionsForWorker(options),
101
- }));
95
+ void this.initializeSession(backend, options);
96
+ }
97
+ async initializeSession(backend, options) {
98
+ try {
99
+ this.backend = await assertSupportedBackend(backend, options);
100
+ this.variant = await providerVariantForBackend(backend, options);
101
+ this.snapshot.backend = this.backend;
102
+ this.snapshot.provider = this.variant;
103
+ this.threadOptionsValue.model =
104
+ typeof options.model === "string" && options.model.trim() ? options.model.trim() : this.backend || "unknown";
105
+ this.child.stdin.write(toSerializablePayload({
106
+ type: "create",
107
+ backend: this.backend,
108
+ options: sanitizeOptionsForWorker(options),
109
+ }));
110
+ }
111
+ catch (error) {
112
+ this.rejectReady?.(error);
113
+ this.resolveReady = null;
114
+ this.rejectReady = null;
115
+ this.rejectPendingRequests(error);
116
+ try {
117
+ this.child.kill("SIGTERM");
118
+ }
119
+ catch {
120
+ // ignore
121
+ }
122
+ }
102
123
  }
103
124
  get threadId() {
104
125
  return this.threadIdValue;
105
126
  }
106
127
  get threadOptions() {
107
- return { ...this.threadOptionsValue };
128
+ const sessionInfo = this.sessionInfo && typeof this.sessionInfo === "object" ? this.sessionInfo : null;
129
+ return {
130
+ ...this.threadOptionsValue,
131
+ ...(sessionInfo?.model ? { model: sessionInfo.model } : {}),
132
+ ...(sessionInfo?.modelProvider ? { modelProvider: sessionInfo.modelProvider } : {}),
133
+ };
108
134
  }
109
135
  getSnapshot() {
110
136
  return {
@@ -352,10 +378,184 @@ export class RemoteAiSession extends EventEmitter {
352
378
  this.pendingRequests.clear();
353
379
  }
354
380
  }
381
+ const LOCAL_SESSION_EVENT_NAMES = ["session", "assistant_message", "working_status", "auth_required", "process.exited"];
382
+ class LocalAiSessionProxy extends EventEmitter {
383
+ constructor(backend, options = {}) {
384
+ super();
385
+ this.backend = undefined;
386
+ this.options = options;
387
+ this.threadIdValue =
388
+ typeof options.resumeSessionId === "string" && options.resumeSessionId.trim()
389
+ ? options.resumeSessionId.trim()
390
+ : "";
391
+ this.threadOptionsValue = {
392
+ model: typeof options.model === "string" && options.model.trim()
393
+ ? options.model.trim()
394
+ : String(backend || "unknown").trim() || "unknown",
395
+ };
396
+ this.useSessionFileReplyStreamValue = true;
397
+ this.sessionInfo = null;
398
+ this.session = null;
399
+ this.closed = false;
400
+ this.sessionMessageHandler = null;
401
+ this.workingStatusHandler = null;
402
+ this.replyTarget = undefined;
403
+ this.snapshot = {
404
+ backend: undefined,
405
+ provider: undefined,
406
+ sessionId: this.threadIdValue || undefined,
407
+ useSessionFileReplyStream: this.useSessionFileReplyStreamValue,
408
+ workerReady: false,
409
+ };
410
+ this.readyPromise = this.initializeSession(backend, options);
411
+ this.readyPromise.catch(() => {
412
+ // keep parity with RemoteAiSession and avoid unhandled rejections
413
+ });
414
+ }
415
+ async initializeSession(backend, options) {
416
+ const session = await createLocalAiSession(backend, options);
417
+ if (this.closed) {
418
+ await session.close?.();
419
+ return session;
420
+ }
421
+ this.session = session;
422
+ for (const eventName of LOCAL_SESSION_EVENT_NAMES) {
423
+ if (typeof session.on === "function") {
424
+ session.on(eventName, async (payload) => {
425
+ if (eventName === "session" && payload && typeof payload === "object") {
426
+ this.sessionInfo = { ...payload };
427
+ if (payload.sessionId) {
428
+ this.threadIdValue = String(payload.sessionId);
429
+ }
430
+ }
431
+ this.emit(eventName, payload);
432
+ });
433
+ }
434
+ }
435
+ if (this.sessionMessageHandler && typeof session.setSessionMessageHandler === "function") {
436
+ session.setSessionMessageHandler(this.sessionMessageHandler);
437
+ }
438
+ if (this.workingStatusHandler && typeof session.setWorkingStatusHandler === "function") {
439
+ session.setWorkingStatusHandler(this.workingStatusHandler);
440
+ }
441
+ if (this.replyTarget !== undefined && typeof session.setSessionReplyTarget === "function") {
442
+ session.setSessionReplyTarget(this.replyTarget);
443
+ }
444
+ const snapshot = typeof session.getSnapshot === "function" ? session.getSnapshot() : {};
445
+ this.backend = snapshot.backend || session.backend || this.backend;
446
+ this.threadIdValue = session.threadId || snapshot.sessionId || this.threadIdValue;
447
+ this.threadOptionsValue = session.threadOptions ? { ...session.threadOptions } : this.threadOptionsValue;
448
+ this.useSessionFileReplyStreamValue =
449
+ typeof session.usesSessionFileReplyStream === "function"
450
+ ? Boolean(session.usesSessionFileReplyStream())
451
+ : snapshot.useSessionFileReplyStream !== undefined
452
+ ? Boolean(snapshot.useSessionFileReplyStream)
453
+ : this.useSessionFileReplyStreamValue;
454
+ this.sessionInfo =
455
+ typeof session.getSessionInfo === "function"
456
+ ? session.getSessionInfo()
457
+ : snapshot.sessionInfo && typeof snapshot.sessionInfo === "object"
458
+ ? { ...snapshot.sessionInfo }
459
+ : this.sessionInfo;
460
+ this.snapshot = {
461
+ ...this.snapshot,
462
+ ...snapshot,
463
+ backend: this.backend,
464
+ sessionId: this.threadIdValue || undefined,
465
+ sessionInfo: this.sessionInfo,
466
+ useSessionFileReplyStream: this.useSessionFileReplyStreamValue,
467
+ workerReady: true,
468
+ };
469
+ return session;
470
+ }
471
+ get threadId() {
472
+ return this.session?.threadId || this.threadIdValue;
473
+ }
474
+ get threadOptions() {
475
+ return this.session?.threadOptions ? { ...this.session.threadOptions } : { ...this.threadOptionsValue };
476
+ }
477
+ getSnapshot() {
478
+ const sessionSnapshot = typeof this.session?.getSnapshot === "function" ? this.session.getSnapshot() : null;
479
+ if (sessionSnapshot) {
480
+ return {
481
+ ...this.snapshot,
482
+ ...sessionSnapshot,
483
+ backend: sessionSnapshot.backend || this.snapshot.backend,
484
+ sessionId: sessionSnapshot.sessionId || this.threadIdValue || undefined,
485
+ sessionInfo: typeof this.session?.getSessionInfo === "function"
486
+ ? this.session.getSessionInfo()
487
+ : sessionSnapshot.sessionInfo || this.sessionInfo || null,
488
+ };
489
+ }
490
+ return {
491
+ ...this.snapshot,
492
+ sessionInfo: this.sessionInfo ? { ...this.sessionInfo } : null,
493
+ };
494
+ }
495
+ usesSessionFileReplyStream() {
496
+ if (typeof this.session?.usesSessionFileReplyStream === "function") {
497
+ return Boolean(this.session.usesSessionFileReplyStream());
498
+ }
499
+ return Boolean(this.useSessionFileReplyStreamValue);
500
+ }
501
+ getSessionInfo() {
502
+ if (typeof this.session?.getSessionInfo === "function") {
503
+ return this.session.getSessionInfo();
504
+ }
505
+ return this.sessionInfo ? { ...this.sessionInfo } : null;
506
+ }
507
+ setSessionMessageHandler(handler) {
508
+ this.sessionMessageHandler = typeof handler === "function" ? handler : null;
509
+ if (typeof this.session?.setSessionMessageHandler === "function") {
510
+ this.session.setSessionMessageHandler(this.sessionMessageHandler);
511
+ }
512
+ }
513
+ setWorkingStatusHandler(handler) {
514
+ this.workingStatusHandler = typeof handler === "function" ? handler : null;
515
+ if (typeof this.session?.setWorkingStatusHandler === "function") {
516
+ this.session.setWorkingStatusHandler(this.workingStatusHandler);
517
+ }
518
+ }
519
+ setSessionReplyTarget(replyTarget) {
520
+ this.replyTarget = replyTarget;
521
+ if (typeof this.session?.setSessionReplyTarget === "function") {
522
+ this.session.setSessionReplyTarget(replyTarget);
523
+ }
524
+ }
525
+ async ensureSessionInfo() {
526
+ const session = await this.readyPromise;
527
+ const sessionInfo = await session.ensureSessionInfo();
528
+ this.sessionInfo = sessionInfo && typeof sessionInfo === "object" ? { ...sessionInfo } : sessionInfo;
529
+ if (sessionInfo?.sessionId) {
530
+ this.threadIdValue = String(sessionInfo.sessionId);
531
+ }
532
+ return sessionInfo;
533
+ }
534
+ async getSessionUsageSummary() {
535
+ const session = await this.readyPromise;
536
+ return await session.getSessionUsageSummary();
537
+ }
538
+ async runTurn(promptText, options = {}) {
539
+ const session = await this.readyPromise;
540
+ return await session.runTurn(promptText, options);
541
+ }
542
+ async close() {
543
+ if (this.closed) {
544
+ return;
545
+ }
546
+ this.closed = true;
547
+ try {
548
+ const session = await this.readyPromise;
549
+ await session.close?.();
550
+ }
551
+ catch {
552
+ // best effort
553
+ }
554
+ }
555
+ }
355
556
  export function createAiSession(backend, options = {}) {
356
- const normalizedBackend = assertSupportedBackend(backend);
357
557
  if (process.env.CONDUCTOR_AI_SDK_DISABLE_WORKER === "1") {
358
- return createLocalAiSession(normalizedBackend, options);
558
+ return new LocalAiSessionProxy(backend, options);
359
559
  }
360
- return new RemoteAiSession(normalizedBackend, options);
560
+ return new RemoteAiSession(backend, options);
361
561
  }
@@ -0,0 +1,4 @@
1
+ export function getExternalProviderRegistry(options?: {}): Promise<any>;
2
+ export function resolveExternalBackend(backend: any, options?: {}): Promise<any>;
3
+ export function getExternalProviderDescriptor(backend: any, options?: {}): Promise<any>;
4
+ export function resetExternalProviderRegistryForTests(): void;
@@ -0,0 +1,143 @@
1
+ import path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { loadEnvConfig } from "./shared.js";
4
+ const BUILT_IN_BACKENDS = new Set(["codex", "claude", "kimi", "opencode"]);
5
+ const registryPromises = new Map();
6
+ let externalProviderImportNonce = 0;
7
+ function normalizeProviderPathEnv(value) {
8
+ return String(value || "").trim();
9
+ }
10
+ function listProviderModulePathsFromValue(rawValue) {
11
+ const raw = normalizeProviderPathEnv(rawValue);
12
+ if (!raw) {
13
+ return [];
14
+ }
15
+ const parts = raw
16
+ .split(process.platform === "win32" ? ";" : ":")
17
+ .map((item) => item.trim())
18
+ .filter(Boolean);
19
+ return [...new Set(parts)];
20
+ }
21
+ function resolveProviderPathEnv(options = {}) {
22
+ const envValue = normalizeProviderPathEnv(process.env.AISDK_PROVIDER_PATH);
23
+ if (envValue) {
24
+ return envValue;
25
+ }
26
+ const envConfig = loadEnvConfig(options.configFile);
27
+ return normalizeProviderPathEnv(envConfig?.AISDK_PROVIDER_PATH);
28
+ }
29
+ function normalizeName(value) {
30
+ return String(value || "").trim().toLowerCase();
31
+ }
32
+ function createRegistry() {
33
+ return {
34
+ descriptors: [],
35
+ byBackend: new Map(),
36
+ aliasToBackend: new Map(),
37
+ };
38
+ }
39
+ function registerAlias(registry, alias, backend, sourcePath) {
40
+ const normalizedAlias = normalizeName(alias);
41
+ if (!normalizedAlias) {
42
+ return;
43
+ }
44
+ if (BUILT_IN_BACKENDS.has(normalizedAlias) && normalizedAlias !== backend) {
45
+ throw new Error(`External AI SDK provider alias "${normalizedAlias}" from ${sourcePath} conflicts with built-in backend "${normalizedAlias}".`);
46
+ }
47
+ const existingBackend = registry.aliasToBackend.get(normalizedAlias);
48
+ if (existingBackend && existingBackend !== backend) {
49
+ throw new Error(`External AI SDK provider alias "${normalizedAlias}" from ${sourcePath} conflicts with backend "${existingBackend}".`);
50
+ }
51
+ registry.aliasToBackend.set(normalizedAlias, backend);
52
+ }
53
+ function validateDescriptor(descriptor, sourcePath) {
54
+ if (!descriptor || typeof descriptor !== "object") {
55
+ throw new Error(`External AI SDK provider module ${sourcePath} contains an invalid provider descriptor.`);
56
+ }
57
+ const backend = normalizeName(descriptor.backend);
58
+ if (!backend) {
59
+ throw new Error(`External AI SDK provider module ${sourcePath} is missing provider.backend.`);
60
+ }
61
+ if (BUILT_IN_BACKENDS.has(backend)) {
62
+ throw new Error(`External AI SDK provider backend "${backend}" from ${sourcePath} conflicts with a built-in backend.`);
63
+ }
64
+ const variant = String(descriptor.variant || "").trim();
65
+ if (!variant) {
66
+ throw new Error(`External AI SDK provider "${backend}" from ${sourcePath} is missing provider.variant.`);
67
+ }
68
+ if (typeof descriptor.createSession !== "function") {
69
+ throw new Error(`External AI SDK provider "${backend}" from ${sourcePath} is missing provider.createSession().`);
70
+ }
71
+ const aliases = Array.isArray(descriptor.aliases) ? descriptor.aliases.map((item) => normalizeName(item)).filter(Boolean) : [];
72
+ return {
73
+ backend,
74
+ variant,
75
+ aliases,
76
+ createSession: descriptor.createSession,
77
+ isSupported: typeof descriptor.isSupported === "function" ? descriptor.isSupported : null,
78
+ sourcePath,
79
+ };
80
+ }
81
+ async function loadRegistry(modulePaths) {
82
+ const registry = createRegistry();
83
+ for (const modulePath of modulePaths) {
84
+ let importedModule;
85
+ try {
86
+ const resolvedPath = path.isAbsolute(modulePath) ? modulePath : path.resolve(modulePath);
87
+ const moduleUrl = pathToFileURL(resolvedPath);
88
+ moduleUrl.searchParams.set("conductor-ai-sdk-provider-attempt", String(++externalProviderImportNonce));
89
+ importedModule = await import(moduleUrl.href);
90
+ }
91
+ catch (error) {
92
+ throw new Error(`Failed to load external AI SDK provider module ${modulePath}: ${error?.message || error}`);
93
+ }
94
+ const providers = Array.isArray(importedModule?.providers) ? importedModule.providers : [];
95
+ if (providers.length === 0) {
96
+ throw new Error(`External AI SDK provider module ${modulePath} must export a non-empty providers array.`);
97
+ }
98
+ for (const rawDescriptor of providers) {
99
+ const descriptor = validateDescriptor(rawDescriptor, modulePath);
100
+ if (registry.byBackend.has(descriptor.backend)) {
101
+ throw new Error(`External AI SDK provider backend "${descriptor.backend}" is declared more than once (latest: ${modulePath}).`);
102
+ }
103
+ registry.descriptors.push(descriptor);
104
+ registry.byBackend.set(descriptor.backend, descriptor);
105
+ registerAlias(registry, descriptor.backend, descriptor.backend, modulePath);
106
+ for (const alias of descriptor.aliases) {
107
+ registerAlias(registry, alias, descriptor.backend, modulePath);
108
+ }
109
+ }
110
+ }
111
+ return registry;
112
+ }
113
+ export async function getExternalProviderRegistry(options = {}) {
114
+ const providerPathEnv = resolveProviderPathEnv(options);
115
+ if (!registryPromises.has(providerPathEnv)) {
116
+ const loadPromise = loadRegistry(listProviderModulePathsFromValue(providerPathEnv)).catch((error) => {
117
+ registryPromises.delete(providerPathEnv);
118
+ throw error;
119
+ });
120
+ registryPromises.set(providerPathEnv, loadPromise);
121
+ }
122
+ return registryPromises.get(providerPathEnv);
123
+ }
124
+ export async function resolveExternalBackend(backend, options = {}) {
125
+ const normalized = normalizeName(backend);
126
+ if (!normalized) {
127
+ return "";
128
+ }
129
+ const registry = await getExternalProviderRegistry(options);
130
+ return registry.aliasToBackend.get(normalized) || normalized;
131
+ }
132
+ export async function getExternalProviderDescriptor(backend, options = {}) {
133
+ const normalized = normalizeName(backend);
134
+ if (!normalized) {
135
+ return null;
136
+ }
137
+ const registry = await getExternalProviderRegistry(options);
138
+ const resolvedBackend = registry.aliasToBackend.get(normalized) || normalized;
139
+ return registry.byBackend.get(resolvedBackend) || null;
140
+ }
141
+ export function resetExternalProviderRegistryForTests() {
142
+ registryPromises.clear();
143
+ }
@@ -39,6 +39,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
39
39
  get threadId(): any;
40
40
  get threadOptions(): {
41
41
  model: any;
42
+ modelProvider: any;
42
43
  };
43
44
  getSnapshot(): {
44
45
  backend: string;
@@ -179,10 +179,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
179
179
  return this.sessionId;
180
180
  }
181
181
  get threadOptions() {
182
- const model = typeof this.options.model === "string" && this.options.model.trim()
183
- ? this.options.model.trim()
184
- : this.backend;
185
- return { model };
182
+ const model = this.sessionInfo?.model ||
183
+ (typeof this.options.model === "string" && this.options.model.trim()
184
+ ? this.options.model.trim()
185
+ : this.backend);
186
+ return {
187
+ model,
188
+ modelProvider: this.sessionInfo?.modelProvider || undefined,
189
+ };
186
190
  }
187
191
  getSnapshot() {
188
192
  return {
@@ -414,10 +418,19 @@ export class ClaudeAgentSdkSession extends EventEmitter {
414
418
  const changed = this.sessionId !== normalizedSessionId;
415
419
  this.sessionId = normalizedSessionId;
416
420
  this.manualResumeReady = true;
421
+ const modelUsage = this.lastResult?.modelUsage && typeof this.lastResult.modelUsage === "object"
422
+ ? this.lastResult.modelUsage
423
+ : null;
424
+ const resolvedModel = typeof modelUsage?.model === "string" && modelUsage.model.trim()
425
+ ? modelUsage.model.trim()
426
+ : typeof this.options.model === "string" && this.options.model.trim()
427
+ ? this.options.model.trim()
428
+ : undefined;
417
429
  this.sessionInfo = {
418
430
  ...(this.sessionInfo || {}),
419
431
  backend: this.backend,
420
432
  sessionId: normalizedSessionId,
433
+ model: resolvedModel,
421
434
  };
422
435
  if (changed) {
423
436
  this.trace(`session ready id=${normalizedSessionId}`);
@@ -37,7 +37,8 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
37
37
  trace(message: any): void;
38
38
  get threadId(): any;
39
39
  get threadOptions(): {
40
- model: string;
40
+ model: any;
41
+ modelProvider: any;
41
42
  };
42
43
  getSnapshot(): {
43
44
  backend: string;
@@ -74,7 +75,7 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
74
75
  getCurrentReplyTarget(): string | undefined;
75
76
  boot(): Promise<void>;
76
77
  bootInternal(): Promise<void>;
77
- applyThreadInfo(thread: any, { resumeReady }?: {
78
+ applyThreadInfo(payload: any, { resumeReady }?: {
78
79
  resumeReady?: boolean | undefined;
79
80
  }): void;
80
81
  applySessionConfigured(params: any): void;
@@ -224,7 +224,10 @@ export class CodexAppServerSession extends EventEmitter {
224
224
  return this.sessionId;
225
225
  }
226
226
  get threadOptions() {
227
- return { model: this.backend };
227
+ return {
228
+ model: this.sessionInfo?.model || this.backend,
229
+ modelProvider: this.sessionInfo?.modelProvider || undefined,
230
+ };
228
231
  }
229
232
  getSnapshot() {
230
233
  return {
@@ -326,9 +329,10 @@ export class CodexAppServerSession extends EventEmitter {
326
329
  else {
327
330
  result = await this.transport.request("thread/start", params);
328
331
  }
329
- this.applyThreadInfo(result?.thread, { resumeReady: Boolean(this.resumeSessionId) });
332
+ this.applyThreadInfo(result, { resumeReady: Boolean(this.resumeSessionId) });
330
333
  }
331
- applyThreadInfo(thread, { resumeReady = false } = {}) {
334
+ applyThreadInfo(payload, { resumeReady = false } = {}) {
335
+ const thread = payload?.thread && typeof payload.thread === "object" ? payload.thread : payload;
332
336
  const threadId = typeof thread?.id === "string" ? thread.id.trim() : "";
333
337
  const threadPath = typeof thread?.path === "string" ? thread.path.trim() : "";
334
338
  if (!threadId) {
@@ -336,15 +340,30 @@ export class CodexAppServerSession extends EventEmitter {
336
340
  }
337
341
  this.sessionId = threadId;
338
342
  this.threadPath = threadPath;
343
+ const resolvedModel = typeof payload?.model === "string" && payload.model.trim()
344
+ ? payload.model.trim()
345
+ : this.sessionInfo?.model || undefined;
346
+ const resolvedModelProvider = typeof payload?.modelProvider === "string" && payload.modelProvider.trim()
347
+ ? payload.modelProvider.trim()
348
+ : typeof thread?.modelProvider === "string" && thread.modelProvider.trim()
349
+ ? thread.modelProvider.trim()
350
+ : this.sessionInfo?.modelProvider || undefined;
351
+ const reasoningEffort = typeof payload?.reasoningEffort === "string" && payload.reasoningEffort.trim()
352
+ ? payload.reasoningEffort.trim()
353
+ : this.sessionInfo?.reasoningEffort || undefined;
339
354
  this.sessionInfo = {
355
+ ...(this.sessionInfo || {}),
340
356
  backend: this.backend,
341
357
  sessionId: threadId,
342
358
  sessionFilePath: threadPath || undefined,
359
+ model: resolvedModel,
360
+ modelProvider: resolvedModelProvider,
361
+ reasoningEffort,
343
362
  };
344
363
  if (resumeReady) {
345
364
  this.manualResumeReady = true;
346
365
  }
347
- this.trace(`thread ready id=${threadId} path="${sanitizeForLog(threadPath, 180)}"`);
366
+ this.trace(`thread ready id=${threadId} path="${sanitizeForLog(threadPath, 180)}" model="${sanitizeForLog(resolvedModel || this.backend, 80)}" provider="${sanitizeForLog(resolvedModelProvider || this.backend, 80)}"`);
348
367
  this.emit("session", this.getSessionInfo());
349
368
  }
350
369
  applySessionConfigured(params) {
@@ -571,7 +590,7 @@ export class CodexAppServerSession extends EventEmitter {
571
590
  }
572
591
  switch (method) {
573
592
  case "thread/started":
574
- this.applyThreadInfo(params?.thread, { resumeReady: Boolean(this.resumeSessionId) });
593
+ this.applyThreadInfo(params, { resumeReady: Boolean(this.resumeSessionId) });
575
594
  return;
576
595
  case "sessionConfigured":
577
596
  case "session_configured":
@@ -60,6 +60,7 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
60
60
  get threadId(): any;
61
61
  get threadOptions(): {
62
62
  model: any;
63
+ modelProvider: any;
63
64
  };
64
65
  getSnapshot(): {
65
66
  backend: string;
@@ -200,10 +200,14 @@ export class OpencodeSdkSession extends EventEmitter {
200
200
  return this.sessionId;
201
201
  }
202
202
  get threadOptions() {
203
- const model = typeof this.options.model === "string" && this.options.model.trim()
204
- ? this.options.model.trim()
205
- : this.backend;
206
- return { model };
203
+ const model = this.sessionInfo?.model ||
204
+ (typeof this.options.model === "string" && this.options.model.trim()
205
+ ? this.options.model.trim()
206
+ : this.backend);
207
+ return {
208
+ model,
209
+ modelProvider: this.sessionInfo?.modelProvider || undefined,
210
+ };
207
211
  }
208
212
  getSnapshot() {
209
213
  return {
@@ -536,9 +540,20 @@ export class OpencodeSdkSession extends EventEmitter {
536
540
  }
537
541
  const changed = this.sessionId !== normalizedSessionId;
538
542
  this.sessionId = normalizedSessionId;
543
+ const resolvedModel = typeof this.lastAssistantInfo?.model?.modelID === "string" && this.lastAssistantInfo.model.modelID.trim()
544
+ ? this.lastAssistantInfo.model.modelID.trim()
545
+ : typeof this.options.model === "string" && this.options.model.trim()
546
+ ? this.options.model.trim()
547
+ : undefined;
548
+ const resolvedModelProvider = typeof this.lastAssistantInfo?.model?.providerID === "string" && this.lastAssistantInfo.model.providerID.trim()
549
+ ? this.lastAssistantInfo.model.providerID.trim()
550
+ : undefined;
539
551
  this.sessionInfo = {
552
+ ...(this.sessionInfo || {}),
540
553
  backend: this.backend,
541
554
  sessionId: normalizedSessionId,
555
+ model: resolvedModel,
556
+ modelProvider: resolvedModelProvider,
542
557
  };
543
558
  if (changed) {
544
559
  this.trace(`session ready id=${normalizedSessionId}`);
@@ -874,6 +889,9 @@ export class OpencodeSdkSession extends EventEmitter {
874
889
  this.activeReplyTarget = "";
875
890
  this.lastUsage = this.buildUsageFromAssistantInfo(currentTurn.lastAssistantInfo);
876
891
  this.lastAssistantInfo = currentTurn.lastAssistantInfo || null;
892
+ this.applySessionInfo({
893
+ id: this.sessionId,
894
+ });
877
895
  await this.emitTerminalWorkingStatus(currentTurn, {
878
896
  phase: "turn_completed",
879
897
  status_done_line: "opencode finished",
@@ -1,8 +1,8 @@
1
- export function normalizeBackend(backend: any): string;
2
- export function isSupportedBackend(backend: any): boolean;
3
- export function providerVariantForBackend(backend: any): "codex-app-server" | "claude-agent-sdk" | "kimi-cli-wire" | "opencode-sdk";
4
- export function assertSupportedBackend(backend: any): "codex" | "claude" | "kimi" | "opencode";
5
- export function createLocalAiSession(backend: any, options?: {}): CodexAppServerSession | ClaudeAgentSdkSession | KimiCliSession | OpencodeSdkSession;
1
+ export function normalizeBackend(backend: any, options?: {}): Promise<any>;
2
+ export function isSupportedBackend(backend: any, options?: {}): Promise<boolean>;
3
+ export function providerVariantForBackend(backend: any, options?: {}): Promise<any>;
4
+ export function assertSupportedBackend(backend: any, options?: {}): Promise<any>;
5
+ export function createLocalAiSession(backend: any, options?: {}): Promise<any>;
6
6
  export const DEFAULT_PROVIDER_VARIANT: "codex-app-server";
7
7
  export const CLAUDE_PROVIDER_VARIANT: "claude-agent-sdk";
8
8
  export const KIMI_PROVIDER_VARIANT: "kimi-cli-wire";
@@ -2,11 +2,12 @@ import { CodexAppServerSession } from "./providers/codex-app-server-session.js";
2
2
  import { ClaudeAgentSdkSession } from "./providers/claude-agent-sdk-session.js";
3
3
  import { KimiCliSession } from "./providers/kimi-cli-session.js";
4
4
  import { OpencodeSdkSession } from "./providers/opencode-sdk-session.js";
5
+ import { getExternalProviderDescriptor, resolveExternalBackend, } from "./external-provider-registry.js";
5
6
  export const DEFAULT_PROVIDER_VARIANT = "codex-app-server";
6
7
  export const CLAUDE_PROVIDER_VARIANT = "claude-agent-sdk";
7
8
  export const KIMI_PROVIDER_VARIANT = "kimi-cli-wire";
8
9
  export const OPENCODE_PROVIDER_VARIANT = "opencode-sdk";
9
- export function normalizeBackend(backend) {
10
+ function normalizeBuiltInBackendName(backend) {
10
11
  const normalized = String(backend || "").trim().toLowerCase();
11
12
  if (normalized === "code") {
12
13
  return "codex";
@@ -22,12 +23,23 @@ export function normalizeBackend(backend) {
22
23
  }
23
24
  return normalized;
24
25
  }
25
- export function isSupportedBackend(backend) {
26
- const normalized = normalizeBackend(backend);
27
- return normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode";
26
+ export async function normalizeBackend(backend, options = {}) {
27
+ const normalized = normalizeBuiltInBackendName(backend);
28
+ if (normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode") {
29
+ return normalized;
30
+ }
31
+ return await resolveExternalBackend(normalized, options);
32
+ }
33
+ export async function isSupportedBackend(backend, options = {}) {
34
+ const normalized = await normalizeBackend(backend, options);
35
+ if (normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode") {
36
+ return true;
37
+ }
38
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
39
+ return Boolean(descriptor);
28
40
  }
29
- export function providerVariantForBackend(backend) {
30
- const normalized = normalizeBackend(backend);
41
+ export async function providerVariantForBackend(backend, options = {}) {
42
+ const normalized = await normalizeBackend(backend, options);
31
43
  if (normalized === "claude") {
32
44
  return CLAUDE_PROVIDER_VARIANT;
33
45
  }
@@ -37,17 +49,28 @@ export function providerVariantForBackend(backend) {
37
49
  if (normalized === "opencode") {
38
50
  return OPENCODE_PROVIDER_VARIANT;
39
51
  }
52
+ if (normalized === "codex") {
53
+ return DEFAULT_PROVIDER_VARIANT;
54
+ }
55
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
56
+ if (descriptor?.variant) {
57
+ return descriptor.variant;
58
+ }
40
59
  return DEFAULT_PROVIDER_VARIANT;
41
60
  }
42
- export function assertSupportedBackend(backend) {
43
- const normalized = normalizeBackend(backend);
61
+ export async function assertSupportedBackend(backend, options = {}) {
62
+ const normalized = await normalizeBackend(backend, options);
44
63
  if (normalized === "codex" || normalized === "claude" || normalized === "kimi" || normalized === "opencode") {
45
64
  return normalized;
46
65
  }
47
- throw new Error(`Unsupported AI SDK backend "${backend}". Only codex app-server, claude agent-sdk, kimi cli wire, and opencode sdk are supported.`);
66
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
67
+ if (descriptor) {
68
+ return normalized;
69
+ }
70
+ throw new Error(`Unsupported AI SDK backend "${backend}". Built-in backends are codex app-server, claude agent-sdk, kimi cli wire, and opencode sdk. Set AISDK_PROVIDER_PATH to load external providers.`);
48
71
  }
49
- export function createLocalAiSession(backend, options = {}) {
50
- const normalized = assertSupportedBackend(backend);
72
+ export async function createLocalAiSession(backend, options = {}) {
73
+ const normalized = await assertSupportedBackend(backend, options);
51
74
  if (normalized === "claude") {
52
75
  return new ClaudeAgentSdkSession(normalized, options);
53
76
  }
@@ -57,7 +80,14 @@ export function createLocalAiSession(backend, options = {}) {
57
80
  if (normalized === "opencode") {
58
81
  return new OpencodeSdkSession(normalized, options);
59
82
  }
60
- return new CodexAppServerSession(normalized, options);
83
+ if (normalized === "codex") {
84
+ return new CodexAppServerSession(normalized, options);
85
+ }
86
+ const descriptor = await getExternalProviderDescriptor(normalized, options);
87
+ if (!descriptor) {
88
+ throw new Error(`External AI SDK provider "${normalized}" is unavailable.`);
89
+ }
90
+ return await descriptor.createSession(normalized, options);
61
91
  }
62
92
  export { CodexAppServerSession };
63
93
  export { ClaudeAgentSdkSession };
package/dist/worker.js CHANGED
@@ -33,7 +33,7 @@ async function handleCreate(message) {
33
33
  throw new Error("AI worker session already created");
34
34
  }
35
35
  sessionCreated = true;
36
- session = createLocalAiSession(message.backend, {
36
+ session = await createLocalAiSession(message.backend, {
37
37
  ...(message.options && typeof message.options === "object" ? message.options : {}),
38
38
  logger: {
39
39
  log: (line) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/ai-sdk",
3
- "version": "0.2.30",
3
+ "version": "0.2.31",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,5 +25,5 @@
25
25
  "@types/node": "^22.10.2",
26
26
  "typescript": "^5.6.3"
27
27
  },
28
- "gitCommitId": "e6a71ad"
28
+ "gitCommitId": "7e0bd83"
29
29
  }