@theokit/sdk 4.9.0 → 4.10.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.
package/dist/eval.cjs CHANGED
@@ -12948,6 +12948,172 @@ function assistantMessage(message) {
12948
12948
  return result;
12949
12949
  }
12950
12950
 
12951
+ // src/internal/llm/pool-aware-client.ts
12952
+ init_errors();
12953
+
12954
+ // src/internal/resilience/circuit-breaker.ts
12955
+ var DEFAULT_MAX_TIMEOUTS = 3;
12956
+ var DEFAULT_COOLDOWN_MS2 = 6e4;
12957
+ var CircuitBreaker = class {
12958
+ constructor(opts = {}) {
12959
+ this.opts = opts;
12960
+ }
12961
+ opts;
12962
+ states = /* @__PURE__ */ new Map();
12963
+ /** @returns true when the breaker is open and the call should be skipped. */
12964
+ shouldSkip(key) {
12965
+ const state2 = this.states.get(key);
12966
+ if (state2 === void 0) return false;
12967
+ if (state2.cooldownUntilMs === 0) return false;
12968
+ if (this.now() < state2.cooldownUntilMs) return true;
12969
+ state2.cooldownUntilMs = 0;
12970
+ state2.consecutiveTimeouts = 0;
12971
+ return false;
12972
+ }
12973
+ recordSuccess(key) {
12974
+ const state2 = this.states.get(key);
12975
+ if (state2 === void 0) return;
12976
+ state2.consecutiveTimeouts = 0;
12977
+ state2.cooldownUntilMs = 0;
12978
+ }
12979
+ recordTimeout(key) {
12980
+ const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
12981
+ state2.consecutiveTimeouts += 1;
12982
+ if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
12983
+ state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
12984
+ }
12985
+ this.states.set(key, state2);
12986
+ }
12987
+ /** @internal — tests inspect counter state. */
12988
+ inspect(key) {
12989
+ return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
12990
+ }
12991
+ now() {
12992
+ return this.opts.now?.() ?? Date.now();
12993
+ }
12994
+ };
12995
+
12996
+ // src/internal/llm/pool-aware-client.ts
12997
+ init_retry();
12998
+ var PoolAwareLlmClient = class {
12999
+ constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
13000
+ this.pool = pool;
13001
+ this.buildClient = buildClient2;
13002
+ this.waitForAvailableMs = waitForAvailableMs;
13003
+ this.name = `pool-aware:${pool.provider}`;
13004
+ this.breaker = resilience.breaker ?? new CircuitBreaker();
13005
+ this.backoffBaseMs = resilience.backoffBaseMs;
13006
+ this.rng = resilience.rng;
13007
+ this.onRateLimit = resilience.onRateLimit;
13008
+ }
13009
+ pool;
13010
+ buildClient;
13011
+ waitForAvailableMs;
13012
+ name;
13013
+ onRateLimit;
13014
+ /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
13015
+ breaker;
13016
+ backoffBaseMs;
13017
+ rng;
13018
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: stream() must serialize pool-select → build client → first-event probe → classify → retry/rotate/propagate. Extracting helpers fragments the linear narrative; the comments above each branch keep it readable.
13019
+ async *stream(request, signal) {
13020
+ if (this.breaker.shouldSkip(this.pool.provider)) {
13021
+ throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
13022
+ code: "circuit_open"
13023
+ });
13024
+ }
13025
+ let hasRetried429 = false;
13026
+ while (true) {
13027
+ if (signal.aborted) throw abortError2(signal);
13028
+ let entry = await this.pool.select();
13029
+ if (entry === null) {
13030
+ if (this.waitForAvailableMs > 0) {
13031
+ const available = await this.pool.waitForAvailable(signal, {
13032
+ maxWaitMs: this.waitForAvailableMs
13033
+ });
13034
+ if (available) {
13035
+ entry = await this.pool.select();
13036
+ }
13037
+ }
13038
+ if (entry === null) {
13039
+ this.breaker.recordTimeout(this.pool.provider);
13040
+ throw new CredentialPoolExhaustedError(
13041
+ `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
13042
+ { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
13043
+ );
13044
+ }
13045
+ }
13046
+ const realClient = this.buildClient(entry.accessToken);
13047
+ const attempt = await tryFirstEvent(realClient, request, signal);
13048
+ if (attempt.kind === "ok") {
13049
+ this.breaker.recordSuccess(this.pool.provider);
13050
+ return yield* relayStream(attempt.generator, attempt.firstResult);
13051
+ }
13052
+ const decision = classifyAndDecide(attempt.error, hasRetried429);
13053
+ if (decision === "retry") {
13054
+ const backoffMs = computeBackoffMs({
13055
+ attempt: 0,
13056
+ ...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
13057
+ ...this.rng !== void 0 ? { rng: this.rng } : {}
13058
+ });
13059
+ this.onRateLimit?.({ attempt: 1, retryAfterMs: backoffMs });
13060
+ await sleepWithAbort(backoffMs, signal);
13061
+ hasRetried429 = true;
13062
+ continue;
13063
+ }
13064
+ if (decision === "rotate") {
13065
+ try {
13066
+ await this.pool.markExhaustedAndRotate({
13067
+ entryId: entry.id,
13068
+ statusCode: attempt.error.metadata?.statusCode ?? inferStatusCode(attempt.error),
13069
+ ...parseRetryAfterMs(attempt.error) !== void 0 ? { resetAtMs: parseRetryAfterMs(attempt.error) } : {}
13070
+ });
13071
+ } catch (persistErr) {
13072
+ process.stderr.write(
13073
+ `[theokit-sdk] credential-pool: persist failed during rotate; continuing in-memory: ${persistErr instanceof Error ? persistErr.message : String(persistErr)}
13074
+ `
13075
+ );
13076
+ }
13077
+ hasRetried429 = false;
13078
+ continue;
13079
+ }
13080
+ this.breaker.recordTimeout(this.pool.provider);
13081
+ throw attempt.error;
13082
+ }
13083
+ }
13084
+ /**
13085
+ * Earliest epoch ms among entries' `lastErrorResetAt` — best estimate
13086
+ * for the caller's `CredentialPoolExhaustedError.nextRetryAt`.
13087
+ */
13088
+ nextRetryHint() {
13089
+ const resets = this.pool.list().map((e) => e.lastErrorResetAt).filter((v) => v !== void 0);
13090
+ return resets.length === 0 ? void 0 : Math.min(...resets);
13091
+ }
13092
+ };
13093
+ function classifyAndDecide(error, hasRetried429) {
13094
+ if (error instanceof NetworkError) return "propagate";
13095
+ if (error instanceof AuthenticationError) return "rotate";
13096
+ const status = error.metadata?.statusCode ?? 429;
13097
+ if (status === 402) return "rotate";
13098
+ return hasRetried429 ? "rotate" : "retry";
13099
+ }
13100
+ function parseRetryAfterMs(error) {
13101
+ const seconds = error.metadata?.retryAfter;
13102
+ if (typeof seconds === "number" && seconds > 0) {
13103
+ return Date.now() + seconds * 1e3;
13104
+ }
13105
+ return void 0;
13106
+ }
13107
+ function inferStatusCode(error) {
13108
+ if (error instanceof AuthenticationError) return 401;
13109
+ if (error instanceof RateLimitError) return 429;
13110
+ return 0;
13111
+ }
13112
+ function abortError2(signal) {
13113
+ if (signal.reason instanceof Error) return signal.reason;
13114
+ return new Error("AbortError");
13115
+ }
13116
+
12951
13117
  // src/internal/llm/responses.ts
12952
13118
  function messageToInputItems(message) {
12953
13119
  const items = [];
@@ -12996,7 +13162,9 @@ function buildResponsesBody(request) {
12996
13162
  for (const message of request.messages) {
12997
13163
  for (const item of messageToInputItems(message)) input.push(item);
12998
13164
  }
12999
- const body = { model: request.model, input, stream: true, store: false };
13165
+ const slash = request.model.lastIndexOf("/");
13166
+ const model = slash >= 0 ? request.model.slice(slash + 1) : request.model;
13167
+ const body = { model, input, stream: true, store: false };
13000
13168
  const instructions = collapseSystemText(request.system);
13001
13169
  if (instructions.length > 0) body.instructions = instructions;
13002
13170
  if (request.maxTokens !== void 0) body.max_output_tokens = request.maxTokens;
@@ -13130,175 +13298,16 @@ var ResponsesApiClient = class {
13130
13298
  }
13131
13299
  if (toolCalls.length > 0 && stopReason === "end_turn") stopReason = "tool_use";
13132
13300
  yield { type: "stop", reason: stopReason };
13133
- return makeLlmFinish({ stopReason, text, toolCalls, inputTokens, outputTokens, reasoningTokens });
13134
- }
13135
- };
13136
-
13137
- // src/internal/llm/pool-aware-client.ts
13138
- init_errors();
13139
-
13140
- // src/internal/resilience/circuit-breaker.ts
13141
- var DEFAULT_MAX_TIMEOUTS = 3;
13142
- var DEFAULT_COOLDOWN_MS2 = 6e4;
13143
- var CircuitBreaker = class {
13144
- constructor(opts = {}) {
13145
- this.opts = opts;
13146
- }
13147
- opts;
13148
- states = /* @__PURE__ */ new Map();
13149
- /** @returns true when the breaker is open and the call should be skipped. */
13150
- shouldSkip(key) {
13151
- const state2 = this.states.get(key);
13152
- if (state2 === void 0) return false;
13153
- if (state2.cooldownUntilMs === 0) return false;
13154
- if (this.now() < state2.cooldownUntilMs) return true;
13155
- state2.cooldownUntilMs = 0;
13156
- state2.consecutiveTimeouts = 0;
13157
- return false;
13158
- }
13159
- recordSuccess(key) {
13160
- const state2 = this.states.get(key);
13161
- if (state2 === void 0) return;
13162
- state2.consecutiveTimeouts = 0;
13163
- state2.cooldownUntilMs = 0;
13164
- }
13165
- recordTimeout(key) {
13166
- const state2 = this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
13167
- state2.consecutiveTimeouts += 1;
13168
- if (state2.consecutiveTimeouts >= (this.opts.maxTimeouts ?? DEFAULT_MAX_TIMEOUTS)) {
13169
- state2.cooldownUntilMs = this.now() + (this.opts.cooldownMs ?? DEFAULT_COOLDOWN_MS2);
13170
- }
13171
- this.states.set(key, state2);
13172
- }
13173
- /** @internal — tests inspect counter state. */
13174
- inspect(key) {
13175
- return this.states.get(key) ?? { consecutiveTimeouts: 0, cooldownUntilMs: 0 };
13176
- }
13177
- now() {
13178
- return this.opts.now?.() ?? Date.now();
13179
- }
13180
- };
13181
-
13182
- // src/internal/llm/pool-aware-client.ts
13183
- init_retry();
13184
- var PoolAwareLlmClient = class {
13185
- constructor(pool, buildClient2, waitForAvailableMs = 3e4, resilience = {}) {
13186
- this.pool = pool;
13187
- this.buildClient = buildClient2;
13188
- this.waitForAvailableMs = waitForAvailableMs;
13189
- this.name = `pool-aware:${pool.provider}`;
13190
- this.breaker = resilience.breaker ?? new CircuitBreaker();
13191
- this.backoffBaseMs = resilience.backoffBaseMs;
13192
- this.rng = resilience.rng;
13193
- this.onRateLimit = resilience.onRateLimit;
13194
- }
13195
- pool;
13196
- buildClient;
13197
- waitForAvailableMs;
13198
- name;
13199
- onRateLimit;
13200
- /** M2 #60 — provider-level circuit breaker (consecutive-failure). */
13201
- breaker;
13202
- backoffBaseMs;
13203
- rng;
13204
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: stream() must serialize pool-select → build client → first-event probe → classify → retry/rotate/propagate. Extracting helpers fragments the linear narrative; the comments above each branch keep it readable.
13205
- async *stream(request, signal) {
13206
- if (this.breaker.shouldSkip(this.pool.provider)) {
13207
- throw new NetworkError(`${this.pool.provider} circuit open \u2014 failing fast`, {
13208
- code: "circuit_open"
13209
- });
13210
- }
13211
- let hasRetried429 = false;
13212
- while (true) {
13213
- if (signal.aborted) throw abortError2(signal);
13214
- let entry = await this.pool.select();
13215
- if (entry === null) {
13216
- if (this.waitForAvailableMs > 0) {
13217
- const available = await this.pool.waitForAvailable(signal, {
13218
- maxWaitMs: this.waitForAvailableMs
13219
- });
13220
- if (available) {
13221
- entry = await this.pool.select();
13222
- }
13223
- }
13224
- if (entry === null) {
13225
- this.breaker.recordTimeout(this.pool.provider);
13226
- throw new CredentialPoolExhaustedError(
13227
- `All ${this.pool.provider} credentials exhausted; next retry available at ${this.nextRetryHint() ?? "unknown"}`,
13228
- { provider: this.pool.provider, nextRetryAt: this.nextRetryHint() }
13229
- );
13230
- }
13231
- }
13232
- const realClient = this.buildClient(entry.accessToken);
13233
- const attempt = await tryFirstEvent(realClient, request, signal);
13234
- if (attempt.kind === "ok") {
13235
- this.breaker.recordSuccess(this.pool.provider);
13236
- return yield* relayStream(attempt.generator, attempt.firstResult);
13237
- }
13238
- const decision = classifyAndDecide(attempt.error, hasRetried429);
13239
- if (decision === "retry") {
13240
- const backoffMs = computeBackoffMs({
13241
- attempt: 0,
13242
- ...this.backoffBaseMs !== void 0 ? { baseMs: this.backoffBaseMs } : {},
13243
- ...this.rng !== void 0 ? { rng: this.rng } : {}
13244
- });
13245
- this.onRateLimit?.({ attempt: 1, retryAfterMs: backoffMs });
13246
- await sleepWithAbort(backoffMs, signal);
13247
- hasRetried429 = true;
13248
- continue;
13249
- }
13250
- if (decision === "rotate") {
13251
- try {
13252
- await this.pool.markExhaustedAndRotate({
13253
- entryId: entry.id,
13254
- statusCode: attempt.error.metadata?.statusCode ?? inferStatusCode(attempt.error),
13255
- ...parseRetryAfterMs(attempt.error) !== void 0 ? { resetAtMs: parseRetryAfterMs(attempt.error) } : {}
13256
- });
13257
- } catch (persistErr) {
13258
- process.stderr.write(
13259
- `[theokit-sdk] credential-pool: persist failed during rotate; continuing in-memory: ${persistErr instanceof Error ? persistErr.message : String(persistErr)}
13260
- `
13261
- );
13262
- }
13263
- hasRetried429 = false;
13264
- continue;
13265
- }
13266
- this.breaker.recordTimeout(this.pool.provider);
13267
- throw attempt.error;
13268
- }
13269
- }
13270
- /**
13271
- * Earliest epoch ms among entries' `lastErrorResetAt` — best estimate
13272
- * for the caller's `CredentialPoolExhaustedError.nextRetryAt`.
13273
- */
13274
- nextRetryHint() {
13275
- const resets = this.pool.list().map((e) => e.lastErrorResetAt).filter((v) => v !== void 0);
13276
- return resets.length === 0 ? void 0 : Math.min(...resets);
13301
+ return makeLlmFinish({
13302
+ stopReason,
13303
+ text,
13304
+ toolCalls,
13305
+ inputTokens,
13306
+ outputTokens,
13307
+ reasoningTokens
13308
+ });
13277
13309
  }
13278
13310
  };
13279
- function classifyAndDecide(error, hasRetried429) {
13280
- if (error instanceof NetworkError) return "propagate";
13281
- if (error instanceof AuthenticationError) return "rotate";
13282
- const status = error.metadata?.statusCode ?? 429;
13283
- if (status === 402) return "rotate";
13284
- return hasRetried429 ? "rotate" : "retry";
13285
- }
13286
- function parseRetryAfterMs(error) {
13287
- const seconds = error.metadata?.retryAfter;
13288
- if (typeof seconds === "number" && seconds > 0) {
13289
- return Date.now() + seconds * 1e3;
13290
- }
13291
- return void 0;
13292
- }
13293
- function inferStatusCode(error) {
13294
- if (error instanceof AuthenticationError) return 401;
13295
- if (error instanceof RateLimitError) return 429;
13296
- return 0;
13297
- }
13298
- function abortError2(signal) {
13299
- if (signal.reason instanceof Error) return signal.reason;
13300
- return new Error("AbortError");
13301
- }
13302
13311
 
13303
13312
  // src/internal/llm/vertex-anthropic.ts
13304
13313
  init_errors();
@@ -13671,6 +13680,9 @@ function resolveApiKey2(envVars) {
13671
13680
  return void 0;
13672
13681
  }
13673
13682
  function selectTransport(profile, apiKey) {
13683
+ const transformCtx = { apiKey };
13684
+ const transformFetch = profile.transform?.fetch?.(transformCtx);
13685
+ const transformHeaders = profile.transform?.headers?.(transformCtx);
13674
13686
  if (profile.apiMode === "chat_completions") {
13675
13687
  if (profile.name === "ollama") {
13676
13688
  const ollamaBase = process.env.OLLAMA_HOST ?? profile.baseUrl;
@@ -13687,6 +13699,7 @@ function selectTransport(profile, apiKey) {
13687
13699
  }
13688
13700
  const envOverride = resolveBaseUrlEnvOverride(profile.name);
13689
13701
  if (envOverride !== void 0) opts.baseUrl = envOverride;
13702
+ if (transformFetch !== void 0) opts.fetch = transformFetch;
13690
13703
  return new OpenAIClient(opts);
13691
13704
  }
13692
13705
  if (profile.apiMode === "anthropic_messages") {
@@ -13703,10 +13716,12 @@ function selectTransport(profile, apiKey) {
13703
13716
  return new BedrockAnthropicClient(realKey !== void 0 ? { apiKey: realKey } : {});
13704
13717
  }
13705
13718
  if (profile.apiMode === "responses_api") {
13719
+ const mergedHeaders = profile.extraHeaders !== void 0 || transformHeaders !== void 0 ? { ...profile.extraHeaders, ...transformHeaders } : void 0;
13706
13720
  return new ResponsesApiClient({
13707
13721
  apiKey,
13708
13722
  ...profile.baseUrl !== void 0 ? { baseUrl: profile.baseUrl } : {},
13709
- ...profile.extraHeaders !== void 0 ? { extraHeaders: profile.extraHeaders } : {},
13723
+ ...mergedHeaders !== void 0 ? { extraHeaders: mergedHeaders } : {},
13724
+ ...transformFetch !== void 0 ? { fetch: transformFetch } : {},
13710
13725
  providerName: profile.name
13711
13726
  });
13712
13727
  }