@gajae-code/ai 0.1.3 → 0.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.2.0] - 2026-05-28
6
+
7
+ ### Fixed
8
+
9
+ - Fixed OpenAI-compatible base URL handling so configured proxy URLs and inherited environment overrides are respected at model discovery, completions, responses, and streaming call sites.
10
+ - Fixed OpenAI direct-provider feature gates so prompt-cache/session behavior uses the resolved Responses base URL and only treats exact default `api.openai.com` hosts/paths as direct OpenAI.
11
+
5
12
  ## [0.1.3] - 2026-05-28
6
13
 
7
14
  ### Changed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.1.3",
4
+ "version": "0.2.0",
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.1.3",
46
+ "@gajae-code/utils": "0.2.0",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -1,4 +1,4 @@
1
- import { $env } from "@gajae-code/utils";
1
+ import { $env, $inheritedEnv } from "@gajae-code/utils";
2
2
  import type { ModelManagerOptions } from "../model-manager";
3
3
  import { Effort } from "../model-thinking";
4
4
  import { getBundledModels } from "../models";
@@ -496,7 +496,11 @@ export interface OpenAIModelManagerConfig {
496
496
 
497
497
  export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses"> {
498
498
  const apiKey = config?.apiKey;
499
- const baseUrl = config?.baseUrl?.trim() || $env.OPENAI_BASE_URL?.trim() || OPENAI_DEFAULT_BASE_URL;
499
+ const baseUrl =
500
+ config?.baseUrl?.trim() ||
501
+ $inheritedEnv("OPENAI_BASE_URL") ||
502
+ $env.OPENAI_BASE_URL?.trim() ||
503
+ OPENAI_DEFAULT_BASE_URL;
500
504
  const references = createBundledReferenceMap<"openai-responses">("openai");
501
505
  return {
502
506
  providerId: "openai",
@@ -1,4 +1,4 @@
1
- import { $env, extractHttpStatusFromError } from "@gajae-code/utils";
1
+ import { $env, $inheritedEnv, extractHttpStatusFromError } from "@gajae-code/utils";
2
2
  import OpenAI from "openai";
3
3
  import type {
4
4
  ChatCompletionAssistantMessageParam,
@@ -71,15 +71,25 @@ import { transformMessages } from "./transform-messages";
71
71
  import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
72
72
 
73
73
  const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
74
+ const OPENAI_DEFAULT_BASE_URL_HOST = "api.openai.com";
75
+
76
+ function isDefaultOpenAIBaseUrl(baseUrl: string): boolean {
77
+ try {
78
+ const url = new URL(baseUrl);
79
+ return url.hostname === OPENAI_DEFAULT_BASE_URL_HOST && (url.pathname === "" || url.pathname === "/v1");
80
+ } catch {
81
+ return baseUrl === OPENAI_DEFAULT_BASE_URL;
82
+ }
83
+ }
74
84
 
75
85
  function resolveOpenAIProviderBaseUrl(
76
86
  baseUrl: string | undefined,
77
87
  authCredentialType: "api_key" | "oauth" | undefined,
78
88
  ): string {
79
89
  if (authCredentialType === "oauth") return OPENAI_DEFAULT_BASE_URL;
80
- const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
90
+ const envBaseUrl = $inheritedEnv("OPENAI_BASE_URL") ?? $env.OPENAI_BASE_URL?.trim();
81
91
  const configuredBaseUrl = baseUrl?.trim();
82
- if (envBaseUrl && (!configuredBaseUrl || configuredBaseUrl.toLowerCase().includes("api.openai.com"))) {
92
+ if (envBaseUrl && (!configuredBaseUrl || isDefaultOpenAIBaseUrl(configuredBaseUrl))) {
83
93
  return envBaseUrl;
84
94
  }
85
95
  return configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
@@ -1,4 +1,4 @@
1
- import { $env, extractHttpStatusFromError, structuredCloneJSON } from "@gajae-code/utils";
1
+ import { $env, $inheritedEnv, extractHttpStatusFromError, structuredCloneJSON } from "@gajae-code/utils";
2
2
  import OpenAI from "openai";
3
3
  import type {
4
4
  Tool as OpenAITool,
@@ -69,11 +69,11 @@ import { transformMessages } from "./transform-messages";
69
69
  * Get prompt cache retention based on cacheRetention and base URL.
70
70
  * Only applies to direct OpenAI API calls (api.openai.com).
71
71
  */
72
- function getPromptCacheRetention(baseUrl: string, cacheRetention: CacheRetention): "24h" | undefined {
72
+ function getPromptCacheRetention(baseUrl: string | undefined, cacheRetention: CacheRetention): "24h" | undefined {
73
73
  if (cacheRetention !== "long") {
74
74
  return undefined;
75
75
  }
76
- if (baseUrl.includes("api.openai.com")) {
76
+ if (baseUrl && isDefaultOpenAIBaseUrl(baseUrl)) {
77
77
  return "24h";
78
78
  }
79
79
  return undefined;
@@ -103,15 +103,34 @@ const OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX = "openai-responses:";
103
103
  const OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE =
104
104
  "OpenAI responses stream timed out while waiting for the first event";
105
105
  const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
106
+ const OPENAI_DEFAULT_BASE_URL_HOST = "api.openai.com";
107
+
108
+ function isDefaultOpenAIBaseUrl(baseUrl: string): boolean {
109
+ try {
110
+ const url = new URL(baseUrl);
111
+ return url.hostname === OPENAI_DEFAULT_BASE_URL_HOST && (url.pathname === "" || url.pathname === "/v1");
112
+ } catch {
113
+ return baseUrl === OPENAI_DEFAULT_BASE_URL;
114
+ }
115
+ }
116
+
117
+ function isOpenAIHostBaseUrl(baseUrl: string): boolean {
118
+ try {
119
+ const url = new URL(baseUrl);
120
+ return url.hostname === OPENAI_DEFAULT_BASE_URL_HOST;
121
+ } catch {
122
+ return baseUrl.toLowerCase().startsWith(OPENAI_DEFAULT_BASE_URL);
123
+ }
124
+ }
106
125
 
107
126
  function resolveOpenAIProviderBaseUrl(
108
127
  baseUrl: string | undefined,
109
128
  authCredentialType: "api_key" | "oauth" | undefined,
110
129
  ): string {
111
130
  if (authCredentialType === "oauth") return OPENAI_DEFAULT_BASE_URL;
112
- const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
131
+ const envBaseUrl = $inheritedEnv("OPENAI_BASE_URL") ?? $env.OPENAI_BASE_URL?.trim();
113
132
  const configuredBaseUrl = baseUrl?.trim();
114
- if (envBaseUrl && (!configuredBaseUrl || configuredBaseUrl.toLowerCase().includes("api.openai.com"))) {
133
+ if (envBaseUrl && (!configuredBaseUrl || isDefaultOpenAIBaseUrl(configuredBaseUrl))) {
115
134
  return envBaseUrl;
116
135
  }
117
136
  return configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
@@ -349,6 +368,9 @@ function createClient(
349
368
 
350
369
  let baseUrl =
351
370
  model.provider === "openai" ? resolveOpenAIProviderBaseUrl(model.baseUrl, authCredentialType) : model.baseUrl;
371
+ if (model.provider === "openai" && !baseUrl) {
372
+ baseUrl = OPENAI_DEFAULT_BASE_URL;
373
+ }
352
374
  if (model.provider === "github-copilot") {
353
375
  apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
354
376
  const hasImages = hasCopilotVisionInput(context.messages);
@@ -363,7 +385,7 @@ function createClient(
363
385
  copilotPremiumRequests = copilot.premiumRequests;
364
386
  baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
365
387
  }
366
- if (sessionId && model.provider === "openai" && (baseUrl ?? "").toLowerCase().includes("api.openai.com")) {
388
+ if (sessionId && model.provider === "openai" && (!model.baseUrl || (baseUrl && isDefaultOpenAIBaseUrl(baseUrl)))) {
367
389
  headers.session_id ??= sessionId;
368
390
  headers["x-client-request-id"] ??= sessionId;
369
391
  }
@@ -411,7 +433,9 @@ function buildParams(
411
433
  const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
412
434
  let systemInstructions: string | undefined;
413
435
  if (systemPrompts.length > 0) {
414
- const needsDeveloperRole = model.reasoning && supportsDeveloperRole(resolvedBaseUrl ?? model);
436
+ const needsDeveloperRole =
437
+ model.reasoning &&
438
+ ((model.provider === "openai" && !model.baseUrl) || supportsDeveloperRole(resolvedBaseUrl || model));
415
439
  if (needsDeveloperRole) {
416
440
  // Reasoning models on known OpenAI-compatible endpoints require the
417
441
  // `developer` role. Send all system prompts inline in `input`.
@@ -434,7 +458,9 @@ function buildParams(
434
458
  instructions: systemInstructions,
435
459
  stream: true,
436
460
  prompt_cache_key: promptCacheKey,
437
- prompt_cache_retention: promptCacheKey ? getPromptCacheRetention(model.baseUrl, cacheRetention) : undefined,
461
+ prompt_cache_retention: promptCacheKey
462
+ ? getPromptCacheRetention(resolvedBaseUrl || model.baseUrl, cacheRetention)
463
+ : undefined,
438
464
  store: false,
439
465
  stream_options: model.provider === "openai" ? { include_obfuscation: false } : undefined,
440
466
  };
@@ -482,24 +508,28 @@ function isAzureOpenAIBaseUrl(baseUrl: string): boolean {
482
508
  function supportsStrictMode(model: Model<"openai-responses">): boolean {
483
509
  if (model.provider === "openai" || model.provider === "azure" || model.provider === "github-copilot") return true;
484
510
 
485
- const baseUrl = model.baseUrl.toLowerCase();
511
+ const baseUrl = model.baseUrl;
512
+ const lowerBaseUrl = baseUrl.toLowerCase();
486
513
  return (
487
- baseUrl.includes("api.openai.com") ||
488
- baseUrl.includes(".openai.azure.com") ||
489
- baseUrl.includes("models.inference.ai.azure.com")
514
+ isDefaultOpenAIBaseUrl(baseUrl) ||
515
+ lowerBaseUrl.includes(".openai.azure.com") ||
516
+ lowerBaseUrl.includes("models.inference.ai.azure.com")
490
517
  );
491
518
  }
492
519
 
493
520
  export function supportsDeveloperRole(modelOrBaseUrl: Pick<Model, "provider" | "baseUrl"> | string): boolean {
494
- const baseUrl =
495
- typeof modelOrBaseUrl === "string" ? modelOrBaseUrl.toLowerCase() : (modelOrBaseUrl.baseUrl ?? "").toLowerCase();
521
+ const baseUrl = typeof modelOrBaseUrl === "string" ? modelOrBaseUrl : (modelOrBaseUrl.baseUrl ?? "");
522
+ if (typeof modelOrBaseUrl !== "string" && modelOrBaseUrl.provider === "openai" && !baseUrl) {
523
+ return true;
524
+ }
525
+ const lowerBaseUrl = baseUrl.toLowerCase();
496
526
  return (
497
- baseUrl.includes("api.openai.com") ||
498
- baseUrl.includes(".openai.azure.com") ||
499
- baseUrl.includes("azure.com/openai") ||
500
- baseUrl.includes("models.inference.ai.azure.com") ||
501
- baseUrl.includes("githubcopilot.com") ||
502
- baseUrl.includes("copilot-api.")
527
+ isOpenAIHostBaseUrl(baseUrl) ||
528
+ lowerBaseUrl.includes(".openai.azure.com") ||
529
+ lowerBaseUrl.includes("azure.com/openai") ||
530
+ lowerBaseUrl.includes("models.inference.ai.azure.com") ||
531
+ lowerBaseUrl.includes("githubcopilot.com") ||
532
+ lowerBaseUrl.includes("copilot-api.")
503
533
  );
504
534
  }
505
535
 
package/src/stream.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { $env, $pickenv, extractHttpStatusFromError } from "@gajae-code/utils";
4
+ import { $env, $inheritedEnv, $pickenv, extractHttpStatusFromError } from "@gajae-code/utils";
5
5
  import { getCustomApi } from "./api-registry";
6
6
  import type { Effort } from "./model-thinking";
7
7
  import {
@@ -77,7 +77,7 @@ type KeyResolver = string | (() => string | undefined);
77
77
 
78
78
  const serviceProviderMap: Record<string, KeyResolver> = {
79
79
  "alibaba-coding-plan": "ALIBABA_CODING_PLAN_API_KEY",
80
- openai: "OPENAI_API_KEY",
80
+ openai: () => $inheritedEnv("OPENAI_API_KEY") ?? $env.OPENAI_API_KEY,
81
81
  google: "GEMINI_API_KEY",
82
82
  groq: "GROQ_API_KEY",
83
83
  cerebras: "CEREBRAS_API_KEY",