@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.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
511
|
+
const baseUrl = model.baseUrl;
|
|
512
|
+
const lowerBaseUrl = baseUrl.toLowerCase();
|
|
486
513
|
return (
|
|
487
|
-
baseUrl
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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",
|