@gajae-code/ai 0.6.1 → 0.6.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.2] - 2026-06-19
6
+ ### Added
7
+
8
+ - Added opt-in `compat.sendSessionHeaders` for the `openai-completions` provider. When enabled (default off), the agent session id is forwarded as vendor-neutral `session_id` and `x-session-id` request headers to any OpenAI-compatible endpoint, letting relays/proxies do session-affinity routing and reuse a server-side prompt cache keyed by the session. Previously only the `openai-responses` provider injected session headers, and only against a first-party OpenAI base URL. Injection runs after the caller's `headers`/`extraHeaders` are merged and before `requestTransform`, and uses `??=` so any header the caller already set always wins; it is skipped entirely when the flag is off or no session id is available, leaving existing provider behavior byte-identical. The first-party `openai-responses` gating is unchanged.
9
+
10
+ ### Fixed
11
+
12
+ - Prevented OpenAI Codex Responses `invalid_function_parameters` / tool-schema validation error events from being treated as retryable `server_error`s, so malformed request schemas fail fast instead of burning the full retry budget.
13
+ - Read LM Studio `/v1/models` nested metadata such as `meta.n_ctx`, `meta.n_ctx_train`, and `details.max_tokens` when normalizing dynamically discovered GGUF-backed local models.
14
+ - Corrected the bundled `openai-codex/gpt-5.5` context window from an overstated 400K back to its true 272K (272,000-token) window, so context-cap / auto-compaction thresholds no longer let gpt-5.5 sessions overrun the model's real limit before compaction (#873).
15
+
5
16
  ## [0.6.1] - 2026-06-18
6
17
 
7
18
  ### Fixed
package/README.md CHANGED
@@ -778,6 +778,7 @@ The `openai-completions` API is implemented by many providers with minor differe
778
778
  interface OpenAICompat {
779
779
  supportsStore?: boolean; // Whether provider supports the `store` field (default: true)
780
780
  supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
781
+ sendSessionHeaders?: boolean; // Forward the session id as `session_id`/`x-session-id` headers for relay session-affinity & prompt-cache reuse (default: false)
781
782
  supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
782
783
  maxTokensField?: "max_completion_tokens" | "max_tokens"; // Which field name to use (default: max_completion_tokens)
783
784
  extraBody?: Record<string, unknown>; // Extra request-body fields for custom proxy routing or provider-specific options
@@ -606,6 +606,17 @@ export interface OpenAICompat extends ToolChoiceCompat {
606
606
  supportsStore?: boolean;
607
607
  /** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
608
608
  supportsDeveloperRole?: boolean;
609
+ /**
610
+ * Whether to forward the agent session id as vendor-neutral session-identity
611
+ * headers (`session_id`, `x-session-id`) on every chat-completions request.
612
+ * Off by default. Opt in for OpenAI-compatible proxies/relays that route on
613
+ * session affinity or reuse a server-side prompt cache keyed by session.
614
+ * First-party OpenAI does not need this (it has its own gated injection in
615
+ * the openai-responses provider). Headers are only added when a non-empty
616
+ * session id is available and are never allowed to overwrite a header the
617
+ * caller already set via `headers`/`requestTransform`.
618
+ */
619
+ sendSessionHeaders?: boolean;
609
620
  /**
610
621
  * Whether the provider's chat-completions endpoint accepts multiple
611
622
  * leading `system`/`developer` messages. When false, ordered system
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.6.1",
4
+ "version": "0.6.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.6.1",
46
+ "@gajae-code/utils": "0.6.2",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
package/src/models.json CHANGED
@@ -53332,7 +53332,7 @@
53332
53332
  "cacheRead": 0.5,
53333
53333
  "cacheWrite": 0
53334
53334
  },
53335
- "contextWindow": 400000,
53335
+ "contextWindow": 272000,
53336
53336
  "maxTokens": 128000,
53337
53337
  "preferWebsockets": true,
53338
53338
  "priority": 9,
@@ -176,6 +176,57 @@ function mapWithBundledReference<TApi extends Api>(
176
176
  };
177
177
  }
178
178
 
179
+ function getNestedModelValue(entry: OpenAICompatibleModelRecord, path: readonly string[]): unknown {
180
+ let current: unknown = entry;
181
+ for (const segment of path) {
182
+ if (!isRecord(current)) {
183
+ return undefined;
184
+ }
185
+ current = current[segment];
186
+ }
187
+ return current;
188
+ }
189
+
190
+ function firstPositiveModelNumber(fallback: number, ...candidates: readonly unknown[]): number {
191
+ for (const candidate of candidates) {
192
+ const value = toNumber(candidate);
193
+ if (value !== undefined && value > 0) {
194
+ return value;
195
+ }
196
+ }
197
+ return fallback;
198
+ }
199
+
200
+ function mapLmStudioModel(
201
+ entry: OpenAICompatibleModelRecord,
202
+ defaults: Model<"openai-completions">,
203
+ reference: Model<"openai-completions"> | undefined,
204
+ ): Model<"openai-completions"> {
205
+ const model = mapWithBundledReference(entry, defaults, reference);
206
+ return {
207
+ ...model,
208
+ contextWindow: firstPositiveModelNumber(
209
+ model.contextWindow,
210
+ entry.context_length,
211
+ entry.max_context_length,
212
+ getNestedModelValue(entry, ["meta", "n_ctx"]),
213
+ getNestedModelValue(entry, ["details", "context_length"]),
214
+ getNestedModelValue(entry, ["details", "n_ctx"]),
215
+ getNestedModelValue(entry, ["meta", "n_ctx_train"]),
216
+ ),
217
+ maxTokens: firstPositiveModelNumber(
218
+ model.maxTokens,
219
+ entry.max_completion_tokens,
220
+ entry.max_tokens,
221
+ entry.max_output_tokens,
222
+ getNestedModelValue(entry, ["details", "max_completion_tokens"]),
223
+ getNestedModelValue(entry, ["details", "max_tokens"]),
224
+ getNestedModelValue(entry, ["meta", "max_completion_tokens"]),
225
+ getNestedModelValue(entry, ["meta", "max_tokens"]),
226
+ ),
227
+ };
228
+ }
229
+
179
230
  function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
180
231
  const value = baseUrl?.trim();
181
232
  if (!value) {
@@ -1215,7 +1266,7 @@ export function lmStudioModelManagerOptions(
1215
1266
  apiKey,
1216
1267
  mapModel: (entry, defaults) => {
1217
1268
  const reference = references.get(defaults.id);
1218
- return mapWithBundledReference(entry, defaults, reference);
1269
+ return mapLmStudioModel(entry, defaults, reference);
1219
1270
  },
1220
1271
  }),
1221
1272
  };
@@ -104,6 +104,14 @@ const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
104
104
  const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
105
105
  const CODEX_PREVIOUS_RESPONSE_STALE_CODES = new Set(["previous_response_not_found", "codex_previous_response_stale"]);
106
106
  const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
107
+ const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
108
+ "invalid_function_parameters",
109
+ "invalid_request_error",
110
+ "invalid_schema",
111
+ "invalid_tool_schema",
112
+ ]);
113
+ const CODEX_NON_RETRYABLE_EVENT_MESSAGE =
114
+ /invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?/i;
107
115
  const CODEX_RETRYABLE_EVENT_MESSAGE =
108
116
  /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
109
117
  const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
@@ -2709,11 +2717,17 @@ class CodexProviderStreamError extends Error {
2709
2717
  }
2710
2718
 
2711
2719
  function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
2712
- const code = getCodexEventErrorCode(rawEvent);
2713
- if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
2720
+ const code = getCodexEventErrorCode(rawEvent).toLowerCase();
2721
+ const message = getCodexEventErrorMessage(rawEvent);
2722
+ if (
2723
+ (code && CODEX_NON_RETRYABLE_EVENT_CODES.has(code)) ||
2724
+ (!!message && CODEX_NON_RETRYABLE_EVENT_MESSAGE.test(message))
2725
+ ) {
2726
+ return false;
2727
+ }
2728
+ if (code && CODEX_RETRYABLE_EVENT_CODES.has(code)) {
2714
2729
  return true;
2715
2730
  }
2716
- const message = getCodexEventErrorMessage(rawEvent);
2717
2731
  return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
2718
2732
  }
2719
2733
 
@@ -189,6 +189,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
189
189
  return {
190
190
  supportsStore: !isNonStandard,
191
191
  supportsDeveloperRole: !isNonStandard,
192
+ sendSessionHeaders: false,
192
193
  supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
193
194
  supportsReasoningEffort: !isGrok && !isZai,
194
195
  reasoningEffortMap,
@@ -254,6 +255,7 @@ export function resolveOpenAICompat(
254
255
  return {
255
256
  supportsStore: model.compat.supportsStore ?? detected.supportsStore,
256
257
  supportsDeveloperRole: model.compat.supportsDeveloperRole ?? detected.supportsDeveloperRole,
258
+ sendSessionHeaders: model.compat.sendSessionHeaders ?? detected.sendSessionHeaders,
257
259
  supportsMultipleSystemMessages:
258
260
  model.compat.supportsMultipleSystemMessages ?? detected.supportsMultipleSystemMessages,
259
261
  supportsReasoningEffort: model.compat.supportsReasoningEffort ?? detected.supportsReasoningEffort,
@@ -453,6 +453,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
453
453
  options?.streamFirstEventTimeoutMs,
454
454
  options?.authCredentialType,
455
455
  options?.requestMaxRetries,
456
+ options?.sessionId,
456
457
  );
457
458
  const premiumRequestsTotal = copilotPremiumRequests;
458
459
  getCapturedErrorResponse = captureErrorResponse;
@@ -943,6 +944,7 @@ async function createClient(
943
944
  streamFirstEventTimeoutOverride?: number,
944
945
  authCredentialType?: OpenAICompletionsOptions["authCredentialType"],
945
946
  requestMaxRetries?: number,
947
+ sessionId?: string,
946
948
  ): Promise<{
947
949
  client: OpenAI;
948
950
  copilotPremiumRequests: number | undefined;
@@ -981,6 +983,14 @@ async function createClient(
981
983
  headers["X-OpenRouter-Cache-TTL"] = "3600";
982
984
  }
983
985
  Object.assign(headers, extraHeaders);
986
+ if (sessionId && resolveOpenAICompat(model).sendSessionHeaders) {
987
+ // Forward the agent session id as vendor-neutral session-identity headers so
988
+ // OpenAI-compatible proxies/relays can do session-affinity routing and reuse a
989
+ // server-side prompt cache. Opt-in via `compat.sendSessionHeaders`; never
990
+ // overwrite a header the caller already set (model.headers / requestTransform).
991
+ headers.session_id ??= sessionId;
992
+ headers["x-session-id"] ??= sessionId;
993
+ }
984
994
  if (model.provider === "kimi-code") {
985
995
  headers = { ...getKimiCommonHeaders(), ...headers };
986
996
  }
package/src/types.ts CHANGED
@@ -742,6 +742,17 @@ export interface OpenAICompat extends ToolChoiceCompat {
742
742
  supportsStore?: boolean;
743
743
  /** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
744
744
  supportsDeveloperRole?: boolean;
745
+ /**
746
+ * Whether to forward the agent session id as vendor-neutral session-identity
747
+ * headers (`session_id`, `x-session-id`) on every chat-completions request.
748
+ * Off by default. Opt in for OpenAI-compatible proxies/relays that route on
749
+ * session affinity or reuse a server-side prompt cache keyed by session.
750
+ * First-party OpenAI does not need this (it has its own gated injection in
751
+ * the openai-responses provider). Headers are only added when a non-empty
752
+ * session id is available and are never allowed to overwrite a header the
753
+ * caller already set via `headers`/`requestTransform`.
754
+ */
755
+ sendSessionHeaders?: boolean;
745
756
  /**
746
757
  * Whether the provider's chat-completions endpoint accepts multiple
747
758
  * leading `system`/`developer` messages. When false, ordered system