@gajae-code/ai 0.1.3 → 0.2.1
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 +13 -0
- package/dist/types/providers/openai-request-transform.d.ts +4 -0
- package/dist/types/types.d.ts +14 -0
- package/package.json +2 -2
- package/src/provider-models/openai-compat.ts +6 -2
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/openai-completions.ts +31 -6
- package/src/providers/openai-request-transform.ts +135 -0
- package/src/providers/openai-responses.ts +71 -23
- package/src/stream.ts +2 -2
- package/src/types.ts +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.1] - 2026-05-30
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Refreshed AI package metadata for the GJC 0.2.1 release.
|
|
10
|
+
|
|
11
|
+
## [0.2.0] - 2026-05-28
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
|
|
5
18
|
## [0.1.3] - 2026-05-28
|
|
6
19
|
|
|
7
20
|
### Changed
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FetchImpl, ModelRequestTransform } from "../types";
|
|
2
|
+
export declare function applyOpenAIRequestTransformHeaders(headers: Record<string, string>, transform: ModelRequestTransform | undefined, profileUserAgent: string): Record<string, string>;
|
|
3
|
+
export declare function applyOpenAIRequestTransformBody(params: object, transform: ModelRequestTransform | undefined): void;
|
|
4
|
+
export declare function wrapFetchForOpenAIRequestTransform(baseFetch: FetchImpl, transform: ModelRequestTransform | undefined, profileUserAgent: string): FetchImpl;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -689,6 +689,16 @@ export interface VercelGatewayRouting {
|
|
|
689
689
|
/** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */
|
|
690
690
|
order?: string[];
|
|
691
691
|
}
|
|
692
|
+
export interface ModelRequestTransform {
|
|
693
|
+
/** Named request-shaping preset. `openai-proxy` removes OpenAI SDK telemetry headers and uses a generic Gajae-Code User-Agent. */
|
|
694
|
+
profile?: "openai-proxy";
|
|
695
|
+
/** Header names to remove from the final outbound request. Case-insensitive. */
|
|
696
|
+
stripHeaders?: string[];
|
|
697
|
+
/** Headers to set after stripping; use null to remove a header explicitly. */
|
|
698
|
+
setHeaders?: Record<string, string | null>;
|
|
699
|
+
/** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
|
|
700
|
+
extraBody?: Record<string, unknown>;
|
|
701
|
+
}
|
|
692
702
|
export interface Model<TApi extends Api = any> {
|
|
693
703
|
id: string;
|
|
694
704
|
name: string;
|
|
@@ -727,6 +737,10 @@ export interface Model<TApi extends Api = any> {
|
|
|
727
737
|
preferWebsockets?: boolean;
|
|
728
738
|
/** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
|
|
729
739
|
contextPromotionTarget?: string;
|
|
740
|
+
/** Provider-facing model id when it differs from the local selector id. */
|
|
741
|
+
wireModelId?: string;
|
|
742
|
+
/** Declarative request shaping for OpenAI-compatible proxy providers. */
|
|
743
|
+
requestTransform?: ModelRequestTransform;
|
|
730
744
|
/** Provider-assigned priority value (lower = higher priority). */
|
|
731
745
|
priority?: number;
|
|
732
746
|
/** Canonical thinking capability metadata for this model. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
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
|
|
46
|
+
"@gajae-code/utils": "0.2.1",
|
|
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",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
|
|
5
5
|
*/
|
|
6
6
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
7
|
-
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.
|
|
7
|
+
const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.44.1";
|
|
8
8
|
const platform = process.platform === "win32" ? "win32" : process.platform;
|
|
9
9
|
const arch = process.arch === "x64" ? "x64" : process.arch;
|
|
10
10
|
return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
|
|
@@ -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,
|
|
@@ -66,20 +66,35 @@ import {
|
|
|
66
66
|
resolveGitHubCopilotBaseUrl,
|
|
67
67
|
} from "./github-copilot-headers";
|
|
68
68
|
import { detectOpenAICompat, type ResolvedOpenAICompat, resolveOpenAICompat } from "./openai-completions-compat";
|
|
69
|
+
import {
|
|
70
|
+
applyOpenAIRequestTransformBody,
|
|
71
|
+
applyOpenAIRequestTransformHeaders,
|
|
72
|
+
wrapFetchForOpenAIRequestTransform,
|
|
73
|
+
} from "./openai-request-transform";
|
|
69
74
|
import { createInitialResponsesAssistantMessage } from "./openai-responses-shared";
|
|
70
75
|
import { transformMessages } from "./transform-messages";
|
|
71
76
|
import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
|
|
72
77
|
|
|
73
78
|
const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
79
|
+
const OPENAI_DEFAULT_BASE_URL_HOST = "api.openai.com";
|
|
80
|
+
|
|
81
|
+
function isDefaultOpenAIBaseUrl(baseUrl: string): boolean {
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL(baseUrl);
|
|
84
|
+
return url.hostname === OPENAI_DEFAULT_BASE_URL_HOST && (url.pathname === "" || url.pathname === "/v1");
|
|
85
|
+
} catch {
|
|
86
|
+
return baseUrl === OPENAI_DEFAULT_BASE_URL;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
74
89
|
|
|
75
90
|
function resolveOpenAIProviderBaseUrl(
|
|
76
91
|
baseUrl: string | undefined,
|
|
77
92
|
authCredentialType: "api_key" | "oauth" | undefined,
|
|
78
93
|
): string {
|
|
79
94
|
if (authCredentialType === "oauth") return OPENAI_DEFAULT_BASE_URL;
|
|
80
|
-
const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
|
|
95
|
+
const envBaseUrl = $inheritedEnv("OPENAI_BASE_URL") ?? $env.OPENAI_BASE_URL?.trim();
|
|
81
96
|
const configuredBaseUrl = baseUrl?.trim();
|
|
82
|
-
if (envBaseUrl && (!configuredBaseUrl || configuredBaseUrl
|
|
97
|
+
if (envBaseUrl && (!configuredBaseUrl || isDefaultOpenAIBaseUrl(configuredBaseUrl))) {
|
|
83
98
|
return envBaseUrl;
|
|
84
99
|
}
|
|
85
100
|
return configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
|
|
@@ -946,6 +961,7 @@ async function createClient(
|
|
|
946
961
|
if (model.provider === "kimi-code") {
|
|
947
962
|
headers = { ...getKimiCommonHeaders(), ...headers };
|
|
948
963
|
}
|
|
964
|
+
headers = applyOpenAIRequestTransformHeaders(headers, model.requestTransform, `Gajae-Code/${packageJson.version}`);
|
|
949
965
|
let copilotPremiumRequests: number | undefined;
|
|
950
966
|
|
|
951
967
|
let baseUrl =
|
|
@@ -1003,7 +1019,14 @@ async function createClient(
|
|
|
1003
1019
|
},
|
|
1004
1020
|
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
1005
1021
|
);
|
|
1006
|
-
const
|
|
1022
|
+
const transformedFetch = wrapFetchForOpenAIRequestTransform(
|
|
1023
|
+
wrappedFetch,
|
|
1024
|
+
model.requestTransform,
|
|
1025
|
+
`Gajae-Code/${packageJson.version}`,
|
|
1026
|
+
);
|
|
1027
|
+
const debugFetch = onSseEvent
|
|
1028
|
+
? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
|
|
1029
|
+
: transformedFetch;
|
|
1007
1030
|
// Bound HTTP request timeout to roughly the first-event watchdog window.
|
|
1008
1031
|
// The OpenAI SDK's default is 10 minutes per attempt × `maxRetries`, which
|
|
1009
1032
|
// turns a stalled-before-headers fetch into a multi-minute hang invisible
|
|
@@ -1068,11 +1091,12 @@ function buildParams(
|
|
|
1068
1091
|
const effectiveMaxTokens = options?.maxTokens ?? (isKimi ? model.maxTokens : undefined);
|
|
1069
1092
|
|
|
1070
1093
|
const requestModelId =
|
|
1071
|
-
model.
|
|
1094
|
+
model.wireModelId ??
|
|
1095
|
+
(model.provider === "fireworks"
|
|
1072
1096
|
? toFireworksWireModelId(model.id)
|
|
1073
1097
|
: model.provider === "firepass"
|
|
1074
1098
|
? toFirepassWireModelId(model.id)
|
|
1075
|
-
: model.id;
|
|
1099
|
+
: model.id);
|
|
1076
1100
|
const params: OpenAICompletionsParams = {
|
|
1077
1101
|
model: requestModelId,
|
|
1078
1102
|
messages,
|
|
@@ -1250,6 +1274,7 @@ function buildParams(
|
|
|
1250
1274
|
if (compat.extraBody) {
|
|
1251
1275
|
Object.assign(params, compat.extraBody);
|
|
1252
1276
|
}
|
|
1277
|
+
applyOpenAIRequestTransformBody(params, model.requestTransform);
|
|
1253
1278
|
|
|
1254
1279
|
return { params, toolStrictMode };
|
|
1255
1280
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { FetchImpl, ModelRequestTransform } from "../types";
|
|
2
|
+
|
|
3
|
+
const PROTECTED_EXTRA_BODY_KEYS = new Set([
|
|
4
|
+
"model",
|
|
5
|
+
"messages",
|
|
6
|
+
"input",
|
|
7
|
+
"instructions",
|
|
8
|
+
"stream",
|
|
9
|
+
"stream_options",
|
|
10
|
+
"store",
|
|
11
|
+
"max_tokens",
|
|
12
|
+
"max_completion_tokens",
|
|
13
|
+
"max_output_tokens",
|
|
14
|
+
"temperature",
|
|
15
|
+
"top_p",
|
|
16
|
+
"presence_penalty",
|
|
17
|
+
"frequency_penalty",
|
|
18
|
+
"reasoning",
|
|
19
|
+
"reasoning_effort",
|
|
20
|
+
"prompt_cache_key",
|
|
21
|
+
"prompt_cache_retention",
|
|
22
|
+
"service_tier",
|
|
23
|
+
"stop",
|
|
24
|
+
"tools",
|
|
25
|
+
"tool_choice",
|
|
26
|
+
"parallel_tool_calls",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const OPENAI_PROXY_STRIP_HEADERS = [
|
|
30
|
+
"x-stainless-arch",
|
|
31
|
+
"x-stainless-async",
|
|
32
|
+
"x-stainless-lang",
|
|
33
|
+
"x-stainless-os",
|
|
34
|
+
"x-stainless-package-version",
|
|
35
|
+
"x-stainless-retry-count",
|
|
36
|
+
"x-stainless-runtime",
|
|
37
|
+
"x-stainless-runtime-version",
|
|
38
|
+
"x-stainless-timeout",
|
|
39
|
+
"x-stainless-helper-method",
|
|
40
|
+
"openai-organization",
|
|
41
|
+
"openai-project",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
function resolveRequestTransform(
|
|
45
|
+
transform: ModelRequestTransform | undefined,
|
|
46
|
+
profileUserAgent: string,
|
|
47
|
+
): ModelRequestTransform | undefined {
|
|
48
|
+
if (!transform) return undefined;
|
|
49
|
+
const profileTransform: ModelRequestTransform =
|
|
50
|
+
transform.profile === "openai-proxy"
|
|
51
|
+
? {
|
|
52
|
+
stripHeaders: [...OPENAI_PROXY_STRIP_HEADERS],
|
|
53
|
+
setHeaders: { "User-Agent": profileUserAgent },
|
|
54
|
+
}
|
|
55
|
+
: {};
|
|
56
|
+
return {
|
|
57
|
+
...profileTransform,
|
|
58
|
+
...transform,
|
|
59
|
+
stripHeaders: transform.stripHeaders ?? profileTransform.stripHeaders,
|
|
60
|
+
setHeaders: transform.setHeaders
|
|
61
|
+
? { ...(profileTransform.setHeaders ?? {}), ...transform.setHeaders }
|
|
62
|
+
: profileTransform.setHeaders,
|
|
63
|
+
extraBody: transform.extraBody,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function deleteHeaders(headers: Headers, names: readonly string[] | undefined): void {
|
|
68
|
+
for (const name of names ?? []) {
|
|
69
|
+
headers.delete(name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setHeaders(headers: Headers, values: Record<string, string | null> | undefined): void {
|
|
74
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
75
|
+
if (value === null) {
|
|
76
|
+
headers.delete(name);
|
|
77
|
+
} else {
|
|
78
|
+
headers.set(name, value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function transformHeaders(
|
|
84
|
+
headers: RequestInit["headers"] | undefined,
|
|
85
|
+
transform: ModelRequestTransform | undefined,
|
|
86
|
+
): Headers {
|
|
87
|
+
const result = new Headers(headers);
|
|
88
|
+
deleteHeaders(result, transform?.stripHeaders);
|
|
89
|
+
setHeaders(result, transform?.setHeaders);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function applyOpenAIRequestTransformHeaders(
|
|
94
|
+
headers: Record<string, string>,
|
|
95
|
+
transform: ModelRequestTransform | undefined,
|
|
96
|
+
profileUserAgent: string,
|
|
97
|
+
): Record<string, string> {
|
|
98
|
+
const resolved = resolveRequestTransform(transform, profileUserAgent);
|
|
99
|
+
if (!resolved) return headers;
|
|
100
|
+
return Object.fromEntries(transformHeaders(headers, resolved).entries());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function applyOpenAIRequestTransformBody(params: object, transform: ModelRequestTransform | undefined): void {
|
|
104
|
+
if (!transform?.extraBody) return;
|
|
105
|
+
const body = params as Record<string, unknown>;
|
|
106
|
+
for (const [key, value] of Object.entries(transform.extraBody)) {
|
|
107
|
+
if (!PROTECTED_EXTRA_BODY_KEYS.has(key) && !(key in body)) {
|
|
108
|
+
body[key] = value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function wrapFetchForOpenAIRequestTransform(
|
|
114
|
+
baseFetch: FetchImpl,
|
|
115
|
+
transform: ModelRequestTransform | undefined,
|
|
116
|
+
profileUserAgent: string,
|
|
117
|
+
): FetchImpl {
|
|
118
|
+
const resolved = resolveRequestTransform(transform, profileUserAgent);
|
|
119
|
+
if (!resolved) return baseFetch;
|
|
120
|
+
return Object.assign(
|
|
121
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
122
|
+
if (input instanceof Request) {
|
|
123
|
+
const request = new Request(input, init);
|
|
124
|
+
deleteHeaders(request.headers, resolved.stripHeaders);
|
|
125
|
+
setHeaders(request.headers, resolved.setHeaders);
|
|
126
|
+
return baseFetch(request);
|
|
127
|
+
}
|
|
128
|
+
return baseFetch(input, {
|
|
129
|
+
...init,
|
|
130
|
+
headers: transformHeaders(init?.headers, resolved),
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
baseFetch.preconnect ? { preconnect: baseFetch.preconnect } : {},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
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,
|
|
5
5
|
ResponseCreateParamsStreaming,
|
|
6
6
|
ResponseInput,
|
|
7
7
|
} from "openai/resources/responses/responses";
|
|
8
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
8
9
|
import { getEnvApiKey } from "../stream";
|
|
9
10
|
import type {
|
|
10
11
|
AssistantMessage,
|
|
@@ -50,6 +51,11 @@ import {
|
|
|
50
51
|
resolveGitHubCopilotBaseUrl,
|
|
51
52
|
} from "./github-copilot-headers";
|
|
52
53
|
import { compactGrammarDefinition } from "./grammar";
|
|
54
|
+
import {
|
|
55
|
+
applyOpenAIRequestTransformBody,
|
|
56
|
+
applyOpenAIRequestTransformHeaders,
|
|
57
|
+
wrapFetchForOpenAIRequestTransform,
|
|
58
|
+
} from "./openai-request-transform";
|
|
53
59
|
import {
|
|
54
60
|
appendResponsesToolResultMessages,
|
|
55
61
|
applyCommonResponsesSamplingParams,
|
|
@@ -69,11 +75,11 @@ import { transformMessages } from "./transform-messages";
|
|
|
69
75
|
* Get prompt cache retention based on cacheRetention and base URL.
|
|
70
76
|
* Only applies to direct OpenAI API calls (api.openai.com).
|
|
71
77
|
*/
|
|
72
|
-
function getPromptCacheRetention(baseUrl: string, cacheRetention: CacheRetention): "24h" | undefined {
|
|
78
|
+
function getPromptCacheRetention(baseUrl: string | undefined, cacheRetention: CacheRetention): "24h" | undefined {
|
|
73
79
|
if (cacheRetention !== "long") {
|
|
74
80
|
return undefined;
|
|
75
81
|
}
|
|
76
|
-
if (baseUrl
|
|
82
|
+
if (baseUrl && isDefaultOpenAIBaseUrl(baseUrl)) {
|
|
77
83
|
return "24h";
|
|
78
84
|
}
|
|
79
85
|
return undefined;
|
|
@@ -103,15 +109,34 @@ const OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX = "openai-responses:";
|
|
|
103
109
|
const OPENAI_RESPONSES_FIRST_EVENT_TIMEOUT_MESSAGE =
|
|
104
110
|
"OpenAI responses stream timed out while waiting for the first event";
|
|
105
111
|
const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
112
|
+
const OPENAI_DEFAULT_BASE_URL_HOST = "api.openai.com";
|
|
113
|
+
|
|
114
|
+
function isDefaultOpenAIBaseUrl(baseUrl: string): boolean {
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(baseUrl);
|
|
117
|
+
return url.hostname === OPENAI_DEFAULT_BASE_URL_HOST && (url.pathname === "" || url.pathname === "/v1");
|
|
118
|
+
} catch {
|
|
119
|
+
return baseUrl === OPENAI_DEFAULT_BASE_URL;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isOpenAIHostBaseUrl(baseUrl: string): boolean {
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(baseUrl);
|
|
126
|
+
return url.hostname === OPENAI_DEFAULT_BASE_URL_HOST;
|
|
127
|
+
} catch {
|
|
128
|
+
return baseUrl.toLowerCase().startsWith(OPENAI_DEFAULT_BASE_URL);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
106
131
|
|
|
107
132
|
function resolveOpenAIProviderBaseUrl(
|
|
108
133
|
baseUrl: string | undefined,
|
|
109
134
|
authCredentialType: "api_key" | "oauth" | undefined,
|
|
110
135
|
): string {
|
|
111
136
|
if (authCredentialType === "oauth") return OPENAI_DEFAULT_BASE_URL;
|
|
112
|
-
const envBaseUrl = $env.OPENAI_BASE_URL?.trim();
|
|
137
|
+
const envBaseUrl = $inheritedEnv("OPENAI_BASE_URL") ?? $env.OPENAI_BASE_URL?.trim();
|
|
113
138
|
const configuredBaseUrl = baseUrl?.trim();
|
|
114
|
-
if (envBaseUrl && (!configuredBaseUrl || configuredBaseUrl
|
|
139
|
+
if (envBaseUrl && (!configuredBaseUrl || isDefaultOpenAIBaseUrl(configuredBaseUrl))) {
|
|
115
140
|
return envBaseUrl;
|
|
116
141
|
}
|
|
117
142
|
return configuredBaseUrl || envBaseUrl || OPENAI_DEFAULT_BASE_URL;
|
|
@@ -344,11 +369,18 @@ function createClient(
|
|
|
344
369
|
}
|
|
345
370
|
const rawApiKey = apiKey;
|
|
346
371
|
|
|
347
|
-
const headers =
|
|
372
|
+
const headers = applyOpenAIRequestTransformHeaders(
|
|
373
|
+
{ ...(model.headers ?? {}), ...(extraHeaders ?? {}) },
|
|
374
|
+
model.requestTransform,
|
|
375
|
+
`Gajae-Code/${packageJson.version}`,
|
|
376
|
+
);
|
|
348
377
|
let copilotPremiumRequests: number | undefined;
|
|
349
378
|
|
|
350
379
|
let baseUrl =
|
|
351
380
|
model.provider === "openai" ? resolveOpenAIProviderBaseUrl(model.baseUrl, authCredentialType) : model.baseUrl;
|
|
381
|
+
if (model.provider === "openai" && !baseUrl) {
|
|
382
|
+
baseUrl = OPENAI_DEFAULT_BASE_URL;
|
|
383
|
+
}
|
|
352
384
|
if (model.provider === "github-copilot") {
|
|
353
385
|
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
|
354
386
|
const hasImages = hasCopilotVisionInput(context.messages);
|
|
@@ -363,11 +395,16 @@ function createClient(
|
|
|
363
395
|
copilotPremiumRequests = copilot.premiumRequests;
|
|
364
396
|
baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl;
|
|
365
397
|
}
|
|
366
|
-
if (sessionId && model.provider === "openai" && (baseUrl
|
|
398
|
+
if (sessionId && model.provider === "openai" && (!model.baseUrl || (baseUrl && isDefaultOpenAIBaseUrl(baseUrl)))) {
|
|
367
399
|
headers.session_id ??= sessionId;
|
|
368
400
|
headers["x-client-request-id"] ??= sessionId;
|
|
369
401
|
}
|
|
370
402
|
const baseFetch = fetchOverride ?? fetch;
|
|
403
|
+
const transformedFetch = wrapFetchForOpenAIRequestTransform(
|
|
404
|
+
baseFetch,
|
|
405
|
+
model.requestTransform,
|
|
406
|
+
`Gajae-Code/${packageJson.version}`,
|
|
407
|
+
);
|
|
371
408
|
return {
|
|
372
409
|
client: new OpenAI({
|
|
373
410
|
apiKey,
|
|
@@ -375,7 +412,9 @@ function createClient(
|
|
|
375
412
|
dangerouslyAllowBrowser: true,
|
|
376
413
|
maxRetries: 5,
|
|
377
414
|
defaultHeaders: headers,
|
|
378
|
-
fetch: onSseEvent
|
|
415
|
+
fetch: onSseEvent
|
|
416
|
+
? wrapFetchForSseDebug(transformedFetch, event => onSseEvent(event, model))
|
|
417
|
+
: transformedFetch,
|
|
379
418
|
}),
|
|
380
419
|
copilotPremiumRequests,
|
|
381
420
|
baseUrl,
|
|
@@ -411,7 +450,9 @@ function buildParams(
|
|
|
411
450
|
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
412
451
|
let systemInstructions: string | undefined;
|
|
413
452
|
if (systemPrompts.length > 0) {
|
|
414
|
-
const needsDeveloperRole =
|
|
453
|
+
const needsDeveloperRole =
|
|
454
|
+
model.reasoning &&
|
|
455
|
+
((model.provider === "openai" && !model.baseUrl) || supportsDeveloperRole(resolvedBaseUrl || model));
|
|
415
456
|
if (needsDeveloperRole) {
|
|
416
457
|
// Reasoning models on known OpenAI-compatible endpoints require the
|
|
417
458
|
// `developer` role. Send all system prompts inline in `input`.
|
|
@@ -429,12 +470,14 @@ function buildParams(
|
|
|
429
470
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
430
471
|
const promptCacheKey = getOpenAIResponsesCacheSessionId(options);
|
|
431
472
|
const params: OpenAIResponsesSamplingParams = {
|
|
432
|
-
model: model.id,
|
|
473
|
+
model: model.wireModelId ?? model.id,
|
|
433
474
|
input: messages,
|
|
434
475
|
instructions: systemInstructions,
|
|
435
476
|
stream: true,
|
|
436
477
|
prompt_cache_key: promptCacheKey,
|
|
437
|
-
prompt_cache_retention: promptCacheKey
|
|
478
|
+
prompt_cache_retention: promptCacheKey
|
|
479
|
+
? getPromptCacheRetention(resolvedBaseUrl || model.baseUrl, cacheRetention)
|
|
480
|
+
: undefined,
|
|
438
481
|
store: false,
|
|
439
482
|
stream_options: model.provider === "openai" ? { include_obfuscation: false } : undefined,
|
|
440
483
|
};
|
|
@@ -464,6 +507,7 @@ function buildParams(
|
|
|
464
507
|
applyResponsesReasoningParams(params, model, options, messages, effort =>
|
|
465
508
|
mapReasoningEffort(effort as NonNullable<OpenAIResponsesOptions["reasoning"]>, model.compat?.reasoningEffortMap),
|
|
466
509
|
);
|
|
510
|
+
applyOpenAIRequestTransformBody(params, model.requestTransform);
|
|
467
511
|
|
|
468
512
|
return { conversationMessages, params };
|
|
469
513
|
}
|
|
@@ -482,24 +526,28 @@ function isAzureOpenAIBaseUrl(baseUrl: string): boolean {
|
|
|
482
526
|
function supportsStrictMode(model: Model<"openai-responses">): boolean {
|
|
483
527
|
if (model.provider === "openai" || model.provider === "azure" || model.provider === "github-copilot") return true;
|
|
484
528
|
|
|
485
|
-
const baseUrl = model.baseUrl
|
|
529
|
+
const baseUrl = model.baseUrl;
|
|
530
|
+
const lowerBaseUrl = baseUrl.toLowerCase();
|
|
486
531
|
return (
|
|
487
|
-
baseUrl
|
|
488
|
-
|
|
489
|
-
|
|
532
|
+
isDefaultOpenAIBaseUrl(baseUrl) ||
|
|
533
|
+
lowerBaseUrl.includes(".openai.azure.com") ||
|
|
534
|
+
lowerBaseUrl.includes("models.inference.ai.azure.com")
|
|
490
535
|
);
|
|
491
536
|
}
|
|
492
537
|
|
|
493
538
|
export function supportsDeveloperRole(modelOrBaseUrl: Pick<Model, "provider" | "baseUrl"> | string): boolean {
|
|
494
|
-
const baseUrl =
|
|
495
|
-
|
|
539
|
+
const baseUrl = typeof modelOrBaseUrl === "string" ? modelOrBaseUrl : (modelOrBaseUrl.baseUrl ?? "");
|
|
540
|
+
if (typeof modelOrBaseUrl !== "string" && modelOrBaseUrl.provider === "openai" && !baseUrl) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
const lowerBaseUrl = baseUrl.toLowerCase();
|
|
496
544
|
return (
|
|
497
|
-
baseUrl
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
545
|
+
isOpenAIHostBaseUrl(baseUrl) ||
|
|
546
|
+
lowerBaseUrl.includes(".openai.azure.com") ||
|
|
547
|
+
lowerBaseUrl.includes("azure.com/openai") ||
|
|
548
|
+
lowerBaseUrl.includes("models.inference.ai.azure.com") ||
|
|
549
|
+
lowerBaseUrl.includes("githubcopilot.com") ||
|
|
550
|
+
lowerBaseUrl.includes("copilot-api.")
|
|
503
551
|
);
|
|
504
552
|
}
|
|
505
553
|
|
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",
|
package/src/types.ts
CHANGED
|
@@ -823,6 +823,18 @@ export interface VercelGatewayRouting {
|
|
|
823
823
|
}
|
|
824
824
|
|
|
825
825
|
// Model interface for the unified model system
|
|
826
|
+
|
|
827
|
+
export interface ModelRequestTransform {
|
|
828
|
+
/** Named request-shaping preset. `openai-proxy` removes OpenAI SDK telemetry headers and uses a generic Gajae-Code User-Agent. */
|
|
829
|
+
profile?: "openai-proxy";
|
|
830
|
+
/** Header names to remove from the final outbound request. Case-insensitive. */
|
|
831
|
+
stripHeaders?: string[];
|
|
832
|
+
/** Headers to set after stripping; use null to remove a header explicitly. */
|
|
833
|
+
setHeaders?: Record<string, string | null>;
|
|
834
|
+
/** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
|
|
835
|
+
extraBody?: Record<string, unknown>;
|
|
836
|
+
}
|
|
837
|
+
|
|
826
838
|
export interface Model<TApi extends Api = any> {
|
|
827
839
|
id: string;
|
|
828
840
|
name: string;
|
|
@@ -861,6 +873,10 @@ export interface Model<TApi extends Api = any> {
|
|
|
861
873
|
preferWebsockets?: boolean;
|
|
862
874
|
/** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
|
|
863
875
|
contextPromotionTarget?: string;
|
|
876
|
+
/** Provider-facing model id when it differs from the local selector id. */
|
|
877
|
+
wireModelId?: string;
|
|
878
|
+
/** Declarative request shaping for OpenAI-compatible proxy providers. */
|
|
879
|
+
requestTransform?: ModelRequestTransform;
|
|
864
880
|
/** Provider-assigned priority value (lower = higher priority). */
|
|
865
881
|
priority?: number;
|
|
866
882
|
/** Canonical thinking capability metadata for this model. */
|