@gajae-code/ai 0.12.0 → 0.12.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 +20 -0
- package/dist/types/model-thinking.d.ts +15 -0
- package/dist/types/providers/azure-openai-responses.d.ts +2 -0
- package/dist/types/providers/google-vertex.d.ts +2 -0
- package/dist/types/utils/fallback-transport.d.ts +2 -0
- package/dist/types/utils/http-inspector.d.ts +10 -0
- package/dist/types/utils/idle-iterator.d.ts +4 -0
- package/dist/types/utils/oauth/kimi.d.ts +2 -0
- package/dist/types/utils/oauth/perplexity.d.ts +2 -7
- package/package.json +2 -2
- package/src/model-thinking.ts +19 -0
- package/src/providers/amazon-bedrock.ts +5 -20
- package/src/providers/anthropic.ts +95 -44
- package/src/providers/azure-openai-responses.ts +28 -16
- package/src/providers/google-vertex.ts +35 -4
- package/src/providers/ollama.ts +32 -4
- package/src/providers/openai-codex-responses.ts +48 -31
- package/src/providers/openai-completions.ts +13 -12
- package/src/providers/openai-responses.ts +9 -11
- package/src/providers/register-builtins.ts +12 -2
- package/src/utils/fallback-transport.ts +17 -10
- package/src/utils/http-inspector.ts +47 -1
- package/src/utils/idle-iterator.ts +12 -3
- package/src/utils/oauth/kimi.ts +17 -2
- package/src/utils/oauth/perplexity.ts +21 -2
- package/src/utils/schema/adapt.ts +2 -2
- package/src/utils/tool-choice-capability.ts +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { APP_NAME, extractHttpStatusFromError, getLogsDir } from "@gajae-code/utils";
|
|
3
4
|
import { isCopilotTransientModelError } from "./retry.js";
|
|
@@ -94,6 +95,49 @@ export function formatModelUnavailableGuidance(dump: RawHttpRequestDump | undefi
|
|
|
94
95
|
].join("\n");
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Cap on retained HTTP 400 request dumps.
|
|
100
|
+
*
|
|
101
|
+
* Each dump carries the full sanitized request body, so they are large: a
|
|
102
|
+
* developer machine accumulated 27,249 files totalling 7.0 GB, averaging 264 KB
|
|
103
|
+
* each, because nothing ever removed them. The rotating application log already
|
|
104
|
+
* bounds itself (`maxSize: 10m`, `maxFiles: 5`); these diagnostics get the same
|
|
105
|
+
* treatment so the newest failures stay available without unbounded growth.
|
|
106
|
+
*/
|
|
107
|
+
const MAX_RETAINED_DUMPS = 50;
|
|
108
|
+
|
|
109
|
+
/** Directory holding the retained HTTP 400 dumps. */
|
|
110
|
+
export function httpRequestDumpDir(): string {
|
|
111
|
+
return path.join(getLogsDir(), "http-400-requests");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Drop the oldest dumps beyond the cap. Best-effort: diagnostics must never turn
|
|
116
|
+
* a request failure into a second failure, so every step swallows its error.
|
|
117
|
+
*
|
|
118
|
+
* File names are `${Date.now()}-${hash}.json`, so a lexical sort is chronological
|
|
119
|
+
* for the millisecond timestamps this writer produces.
|
|
120
|
+
*/
|
|
121
|
+
export async function pruneHttpRequestDumps(dir: string = httpRequestDumpDir()): Promise<number> {
|
|
122
|
+
const entries = await fs.readdir(dir).catch(() => undefined);
|
|
123
|
+
if (!entries) return 0;
|
|
124
|
+
|
|
125
|
+
const dumps = entries.filter(name => name.endsWith(".json")).sort();
|
|
126
|
+
if (dumps.length <= MAX_RETAINED_DUMPS) return 0;
|
|
127
|
+
|
|
128
|
+
let removed = 0;
|
|
129
|
+
for (const name of dumps.slice(0, dumps.length - MAX_RETAINED_DUMPS)) {
|
|
130
|
+
if (
|
|
131
|
+
await fs.rm(path.join(dir, name), { force: true }).then(
|
|
132
|
+
() => true,
|
|
133
|
+
() => false,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
removed++;
|
|
137
|
+
}
|
|
138
|
+
return removed;
|
|
139
|
+
}
|
|
140
|
+
|
|
97
141
|
export async function appendRawHttpRequestDumpFor400(
|
|
98
142
|
message: string,
|
|
99
143
|
error: unknown,
|
|
@@ -105,10 +149,12 @@ export async function appendRawHttpRequestDumpFor400(
|
|
|
105
149
|
|
|
106
150
|
const sanitizedDump = sanitizeDump(dump);
|
|
107
151
|
const fileName = `${Date.now()}-${Bun.hash(JSON.stringify(sanitizedDump)).toString(36)}.json`;
|
|
108
|
-
const
|
|
152
|
+
const dumpDir = httpRequestDumpDir();
|
|
153
|
+
const filePath = path.join(dumpDir, fileName);
|
|
109
154
|
|
|
110
155
|
try {
|
|
111
156
|
await Bun.write(filePath, `${JSON.stringify(sanitizedDump, null, 2)}\n`);
|
|
157
|
+
await pruneHttpRequestDumps(dumpDir);
|
|
112
158
|
return `${message}\nraw-http-request=${filePath}\n${RAW_HTTP_REQUEST_PRIVACY_NOTE}`;
|
|
113
159
|
} catch (writeError) {
|
|
114
160
|
const writeMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { $env } from "@gajae-code/utils";
|
|
2
|
+
import { STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE } from "./fallback-transport";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120_000;
|
|
4
5
|
const DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS = 100_000;
|
|
@@ -66,6 +67,14 @@ export function getStreamFirstEventTimeoutMs(
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export type Watchdog = NodeJS.Timeout | undefined;
|
|
70
|
+
export class FirstEventTimeoutError extends Error {
|
|
71
|
+
readonly providerCode = STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE;
|
|
72
|
+
|
|
73
|
+
constructor(message: string) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "FirstEventTimeoutError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
|
|
70
79
|
const dummyWatchdog = setTimeout(() => {}, 1);
|
|
71
80
|
clearTimeout(dummyWatchdog);
|
|
@@ -228,9 +237,9 @@ export async function* iterateWithIdleTimeout<T>(
|
|
|
228
237
|
options.onFirstItemTimeout?.();
|
|
229
238
|
}
|
|
230
239
|
closeIterator();
|
|
231
|
-
throw
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
throw awaitingFirstItem
|
|
241
|
+
? new FirstEventTimeoutError(options.firstItemErrorMessage ?? options.errorMessage)
|
|
242
|
+
: new Error(options.errorMessage);
|
|
234
243
|
}
|
|
235
244
|
if (outcome.kind === "error") {
|
|
236
245
|
throw outcome.error;
|
package/src/utils/oauth/kimi.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { scheduler } from "node:timers/promises";
|
|
10
|
-
import { $
|
|
10
|
+
import { $pickCredentialEnv, getAgentDir, isEnoent } from "@gajae-code/utils";
|
|
11
11
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
12
12
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
13
13
|
|
|
@@ -38,8 +38,23 @@ interface TokenResponse {
|
|
|
38
38
|
interval?: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* OAuth host for the device flow, from trusted environment sources only.
|
|
43
|
+
*
|
|
44
|
+
* This host receives the device-authorization request, the authorization-code
|
|
45
|
+
* exchange, and the refresh call that carries the existing refresh token, so
|
|
46
|
+
* whatever can set it can collect the user's Kimi credentials. `$env` merges the
|
|
47
|
+
* caller's `cwd/.env`, so reading it there would let repository content redirect
|
|
48
|
+
* the login flow. Resolve it the same way the credentials themselves are:
|
|
49
|
+
* launching shell plus GJC/user-owned `.env` files, never the project `.env`.
|
|
50
|
+
*/
|
|
41
51
|
function resolveOAuthHost(): string {
|
|
42
|
-
return $
|
|
52
|
+
return $pickCredentialEnv("KIMI_CODE_OAUTH_HOST", "KIMI_OAUTH_HOST") || DEFAULT_OAUTH_HOST;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Test seam: the OAuth host as resolved from trusted env. */
|
|
56
|
+
export function resolveKimiOAuthHostForTest(): string {
|
|
57
|
+
return resolveOAuthHost();
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
function formatDeviceModel(system: string, release: string, arch: string): string {
|
|
@@ -186,13 +186,32 @@ async function httpEmailLogin(ctrl: OAuthController): Promise<OAuthCredentials>
|
|
|
186
186
|
*
|
|
187
187
|
* No browser/manual token paste fallback is used.
|
|
188
188
|
*/
|
|
189
|
+
/**
|
|
190
|
+
* Whether the operator disabled borrowing a token from the native macOS app.
|
|
191
|
+
*
|
|
192
|
+
* `GJC_AUTH_NO_BORROW` is the documented name; `PI_AUTH_NO_BORROW` is the legacy
|
|
193
|
+
* one that was the only name actually read.
|
|
194
|
+
*/
|
|
195
|
+
function authBorrowDisabled(): boolean {
|
|
196
|
+
return Boolean($env.GJC_AUTH_NO_BORROW || $env.PI_AUTH_NO_BORROW);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Test seam: the resolved native-app borrowing opt-out. */
|
|
200
|
+
export function authBorrowDisabledForTest(): boolean {
|
|
201
|
+
return authBorrowDisabled();
|
|
202
|
+
}
|
|
203
|
+
|
|
189
204
|
export async function loginPerplexity(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
190
205
|
if (!ctrl.onPrompt) {
|
|
191
206
|
throw new Error("Perplexity login requires onPrompt callback");
|
|
192
207
|
}
|
|
193
208
|
|
|
194
|
-
// Path 1: Native macOS app JWT
|
|
195
|
-
|
|
209
|
+
// Path 1: Native macOS app JWT, skipped when the operator opts out.
|
|
210
|
+
//
|
|
211
|
+
// Presence-based on purpose: this is a privacy opt-out, so any set value must
|
|
212
|
+
// disable borrowing. A boolean contract would let `GJC_AUTH_NO_BORROW=0`
|
|
213
|
+
// silently re-enable reading a token out of another application.
|
|
214
|
+
if (!authBorrowDisabled()) {
|
|
196
215
|
ctrl.onProgress?.("Checking for Perplexity desktop app...");
|
|
197
216
|
const nativeJwt = await extractFromNativeApp();
|
|
198
217
|
if (nativeJwt) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $pickflag } from "@gajae-code/utils";
|
|
2
2
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
3
3
|
import { tryEnforceStrictSchema } from "./normalize";
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ import { tryEnforceStrictSchema } from "./normalize";
|
|
|
11
11
|
* see `openai-completions`, `openai-responses`, `OpenAI code provider-responses`, and
|
|
12
12
|
* the strict candidate selection in `anthropic`.
|
|
13
13
|
*/
|
|
14
|
-
export const NO_STRICT = $
|
|
14
|
+
export const NO_STRICT = $pickflag("GJC_NO_STRICT", "PI_NO_STRICT");
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Consolidated helper for OpenAI-style strict schema enforcement.
|
|
@@ -175,7 +175,8 @@ export function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToo
|
|
|
175
175
|
message,
|
|
176
176
|
) ||
|
|
177
177
|
/forces?\s+tool\s+use.*?(not\s+compatible|incompatible|not\s+supported)/is.test(message) ||
|
|
178
|
-
/does\s+not\s+support\s+forced\s+tool[_\s-]?choices?/is.test(message)
|
|
178
|
+
/does\s+not\s+support\s+forced\s+tool[_\s-]?choices?/is.test(message) ||
|
|
179
|
+
/tool[_\s-]?choices?\s+['"`][^'"`\r\n]+['"`]\s+not\s+found\s+in\s+['"`]tools['"`]\s+parameter\b/is.test(message)
|
|
179
180
|
);
|
|
180
181
|
}
|
|
181
182
|
|