@juvio15/pi-cliproxyapi-provider 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/package.json +51 -4
- package/src/core/catalog.ts +400 -0
- package/src/core/connection.ts +119 -0
- package/src/core/costs.ts +477 -0
- package/src/core/index.ts +186 -0
- package/src/core/oauth.ts +279 -0
- package/src/core/streams.ts +89 -0
- package/src/core/usage.ts +813 -0
- package/src/extension/completions.ts +20 -0
- package/src/extension/config.ts +314 -0
- package/src/extension/fast.ts +81 -0
- package/src/extension/files.ts +146 -0
- package/src/extension/footer.ts +229 -0
- package/src/extension/index.ts +477 -0
- package/src/extension/oauth.ts +203 -0
- package/src/extension/pause.ts +126 -0
- package/src/extension/prices.ts +191 -0
- package/src/extension/prompt-cache.ts +17 -0
- package/src/extension/provider.ts +386 -0
- package/src/extension/retry.ts +41 -0
- package/src/extension/status.ts +166 -0
- package/src/extension/tps.ts +215 -0
- package/src/extension/usage.ts +609 -0
- package/src/extension/web-search.ts +307 -0
- package/src/web-search/endpoint.ts +21 -0
- package/src/web-search/index.ts +21 -0
- package/src/web-search/model-selector.ts +93 -0
- package/src/web-search/search.ts +571 -0
- package/src/web-search/sse.ts +144 -0
- package/README.md +0 -3
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/* oxlint-disable eslint/no-await-in-loop -- OAuth status polling is intentionally sequential so each check sees the latest server state. */
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import type { Static, TSchema } from "typebox";
|
|
6
|
+
import { Value } from "typebox/value";
|
|
7
|
+
|
|
8
|
+
import { resolveEndpoints } from "./connection.ts";
|
|
9
|
+
|
|
10
|
+
/** Shared options for the CLIProxyAPI v8 management OAuth flow. */
|
|
11
|
+
export interface OAuthRequestOptions {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
/** The CLIProxyAPI management key, not the client API key. */
|
|
14
|
+
managementKey: string;
|
|
15
|
+
fetch?: typeof globalThis.fetch;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
/** Per-request timeout. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StartOAuthOptions extends OAuthRequestOptions {
|
|
22
|
+
/**
|
|
23
|
+
* CLIProxyAPI provider id: `claude`, `codex`, `antigravity`, `kimi`,
|
|
24
|
+
* `kimi-ai`, `xai`, `devin`, `meta`, or a registered plugin provider id.
|
|
25
|
+
* Any other non-empty id is passed through for the server to judge.
|
|
26
|
+
*/
|
|
27
|
+
provider: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A started login: open `url`, then poll or cancel with `state`. */
|
|
31
|
+
export interface OAuthStart {
|
|
32
|
+
state: string;
|
|
33
|
+
url: string;
|
|
34
|
+
/** `device` for device-code logins (Kimi, xAI, Meta): no callback URL comes back, so poll the status directly. */
|
|
35
|
+
flow?: string;
|
|
36
|
+
/** The code to enter at `url` during a device login, when the server sends one. */
|
|
37
|
+
userCode?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SubmitOAuthCallbackOptions extends OAuthRequestOptions {
|
|
41
|
+
provider?: string;
|
|
42
|
+
/** The complete callback URL the browser landed on, with its code and state. */
|
|
43
|
+
redirectUrl?: string;
|
|
44
|
+
state?: string;
|
|
45
|
+
code?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PollOAuthStatusOptions extends OAuthRequestOptions {
|
|
50
|
+
state: string;
|
|
51
|
+
/** Delay between status checks while the login is pending. */
|
|
52
|
+
pollIntervalMs?: number;
|
|
53
|
+
/** Longest total wait before the pending login counts as failed. */
|
|
54
|
+
maxWaitMs?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type OAuthStatusValue = "error" | "ok" | "wait";
|
|
58
|
+
|
|
59
|
+
export interface OAuthStatus {
|
|
60
|
+
status: OAuthStatusValue;
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
65
|
+
const DEFAULT_POLL_INTERVAL_MS = 2000;
|
|
66
|
+
const DEFAULT_MAX_WAIT_MS = 5 * 60_000;
|
|
67
|
+
|
|
68
|
+
const StartSchema = Type.Object({
|
|
69
|
+
flow: Type.Optional(Type.String()),
|
|
70
|
+
state: Type.String(),
|
|
71
|
+
status: Type.Optional(Type.String()),
|
|
72
|
+
url: Type.String(),
|
|
73
|
+
user_code: Type.Optional(Type.String()),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const StatusSchema = Type.Object({
|
|
77
|
+
error: Type.Optional(Type.String()),
|
|
78
|
+
status: Type.Union([
|
|
79
|
+
Type.Literal("error"),
|
|
80
|
+
Type.Literal("ok"),
|
|
81
|
+
Type.Literal("wait"),
|
|
82
|
+
]),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const ErrorSchema = Type.Object({
|
|
86
|
+
error: Type.Optional(Type.String()),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const parseJson = <Schema extends TSchema>(
|
|
90
|
+
schema: Schema,
|
|
91
|
+
text: string
|
|
92
|
+
): Static<Schema> | undefined => {
|
|
93
|
+
try {
|
|
94
|
+
const value: unknown = JSON.parse(text);
|
|
95
|
+
|
|
96
|
+
return Value.Check(schema, value) ? value : undefined;
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const nonEmpty = (value: string | undefined, name: string): string => {
|
|
103
|
+
const trimmed = value?.trim() ?? "";
|
|
104
|
+
|
|
105
|
+
if (!trimmed) {
|
|
106
|
+
throw new Error(`${name} is empty`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return trimmed;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const managementUrl = (baseUrl: string): string =>
|
|
113
|
+
`${resolveEndpoints(baseUrl).rootUrl}/v8/management`;
|
|
114
|
+
|
|
115
|
+
const request = async <Schema extends TSchema>(
|
|
116
|
+
options: OAuthRequestOptions,
|
|
117
|
+
route: string,
|
|
118
|
+
schema: Schema,
|
|
119
|
+
init: RequestInit = {}
|
|
120
|
+
): Promise<Static<Schema>> => {
|
|
121
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
122
|
+
const managementKey = nonEmpty(options.managementKey, "managementKey");
|
|
123
|
+
const timeout = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
124
|
+
|
|
125
|
+
const response = await fetchFn(`${managementUrl(options.baseUrl)}${route}`, {
|
|
126
|
+
...init,
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Bearer ${managementKey}`,
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
},
|
|
131
|
+
signal: options.signal
|
|
132
|
+
? AbortSignal.any([options.signal, timeout])
|
|
133
|
+
: timeout,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const [routePath] = route.split("?");
|
|
137
|
+
const text = await response.text();
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const detail = parseJson(ErrorSchema, text)?.error;
|
|
141
|
+
|
|
142
|
+
throw new Error(
|
|
143
|
+
`CLIProxyAPI management ${routePath} returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const value = parseJson(schema, text);
|
|
148
|
+
|
|
149
|
+
if (value === undefined) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`CLIProxyAPI management ${routePath} returned an unexpected shape`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return value;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
interface OAuthCallbackBody {
|
|
159
|
+
provider?: string;
|
|
160
|
+
redirect_url?: string;
|
|
161
|
+
state?: string;
|
|
162
|
+
code?: string;
|
|
163
|
+
error?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Starts an upstream account login. The server answers
|
|
168
|
+
* `{ status, url, state }`: `url` is the authorization URL to open and
|
|
169
|
+
* `state` identifies the pending session for the callback, status, and
|
|
170
|
+
* cancellation calls.
|
|
171
|
+
*/
|
|
172
|
+
export const startOAuth = async (
|
|
173
|
+
options: StartOAuthOptions
|
|
174
|
+
): Promise<OAuthStart> => {
|
|
175
|
+
const provider = nonEmpty(options.provider, "provider");
|
|
176
|
+
|
|
177
|
+
const result = await request(
|
|
178
|
+
options,
|
|
179
|
+
`/oauth/auth-url?provider=${encodeURIComponent(provider)}`,
|
|
180
|
+
StartSchema
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const start: OAuthStart = { state: result.state, url: result.url };
|
|
184
|
+
|
|
185
|
+
if (result.flow) {
|
|
186
|
+
start.flow = result.flow;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result.user_code) {
|
|
190
|
+
start.userCode = result.user_code;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return start;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Submits the callback for a started login, either as the complete
|
|
198
|
+
* `redirect_url` the browser landed on or as a bare `state` plus `code`.
|
|
199
|
+
* Acceptance only hands the values to the pending session; poll the status
|
|
200
|
+
* to learn whether the credential exchange finished.
|
|
201
|
+
*/
|
|
202
|
+
export const submitOAuthCallback = async (
|
|
203
|
+
options: SubmitOAuthCallbackOptions
|
|
204
|
+
): Promise<OAuthStatus> => {
|
|
205
|
+
const body: OAuthCallbackBody = {};
|
|
206
|
+
|
|
207
|
+
if (options.provider?.trim()) {
|
|
208
|
+
body.provider = options.provider.trim();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (options.redirectUrl?.trim()) {
|
|
212
|
+
body.redirect_url = options.redirectUrl.trim();
|
|
213
|
+
} else {
|
|
214
|
+
const state = nonEmpty(options.state, "state");
|
|
215
|
+
const code = options.code?.trim() ?? "";
|
|
216
|
+
const error = options.error?.trim() ?? "";
|
|
217
|
+
|
|
218
|
+
if (!code && !error) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"submitOAuthCallback requires a redirectUrl, or a state with a code or error"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
body.state = state;
|
|
225
|
+
|
|
226
|
+
if (code) {
|
|
227
|
+
body.code = code;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (error) {
|
|
231
|
+
body.error = error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return await request(options, "/oauth/callback", StatusSchema, {
|
|
236
|
+
body: JSON.stringify(body),
|
|
237
|
+
method: "POST",
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Polls a started login until the server reports `ok` or `error`; `wait`
|
|
243
|
+
* means the login is still pending. A wait past `maxWaitMs` comes back as
|
|
244
|
+
* an `error` status, and aborting `signal` rejects instead.
|
|
245
|
+
*/
|
|
246
|
+
export const pollOAuthStatus = async (
|
|
247
|
+
options: PollOAuthStatusOptions
|
|
248
|
+
): Promise<OAuthStatus> => {
|
|
249
|
+
const state = nonEmpty(options.state, "state");
|
|
250
|
+
const interval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
251
|
+
const deadline = Date.now() + (options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS);
|
|
252
|
+
|
|
253
|
+
while (true) {
|
|
254
|
+
options.signal?.throwIfAborted();
|
|
255
|
+
|
|
256
|
+
const result = await request(
|
|
257
|
+
options,
|
|
258
|
+
`/oauth/status?state=${encodeURIComponent(state)}`,
|
|
259
|
+
StatusSchema
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (result.status !== "wait") {
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const remaining = deadline - Date.now();
|
|
267
|
+
|
|
268
|
+
if (remaining <= 0) {
|
|
269
|
+
return {
|
|
270
|
+
error: "Timed out waiting for the CLIProxyAPI OAuth login to complete.",
|
|
271
|
+
status: "error",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await delay(Math.min(interval, remaining), undefined, {
|
|
276
|
+
signal: options.signal,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Model,
|
|
3
|
+
Api,
|
|
4
|
+
ProviderStreams,
|
|
5
|
+
StreamOptions,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
7
|
+
|
|
8
|
+
import type { ThoughtSummary } from "./catalog.ts";
|
|
9
|
+
|
|
10
|
+
export interface PayloadSettings {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
/** Read at request time so /fast changes existing sessions immediately. */
|
|
13
|
+
fast?: () => boolean;
|
|
14
|
+
thoughtSummaries?: Readonly<Record<string, ThoughtSummary>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RequestPayload {
|
|
18
|
+
thinking?: { display?: string };
|
|
19
|
+
service_tier?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const applyPayloadSettings = (
|
|
23
|
+
payload: RequestPayload,
|
|
24
|
+
model: Model<Api>,
|
|
25
|
+
options: StreamOptions | undefined,
|
|
26
|
+
settings: PayloadSettings
|
|
27
|
+
): void => {
|
|
28
|
+
if (model.api === "anthropic-messages") {
|
|
29
|
+
const { thinking } = payload;
|
|
30
|
+
|
|
31
|
+
if (!thinking) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The SDK inserts summarized itself, so presence in the payload is not caller intent.
|
|
36
|
+
const explicit =
|
|
37
|
+
options && "thinkingDisplay" in options
|
|
38
|
+
? options.thinkingDisplay
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
const configured =
|
|
42
|
+
settings.thoughtSummaries?.[model.id] ??
|
|
43
|
+
("thoughtSummary" in model ? model.thoughtSummary : "omitted");
|
|
44
|
+
|
|
45
|
+
if (explicit === "summarized" || explicit === "omitted") {
|
|
46
|
+
thinking.display = explicit;
|
|
47
|
+
} else if (configured === "summarized") {
|
|
48
|
+
thinking.display = configured;
|
|
49
|
+
} else {
|
|
50
|
+
delete thinking.display;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
model.api === "openai-responses" &&
|
|
56
|
+
(settings.fast?.() ?? false) &&
|
|
57
|
+
"supportsFast" in model &&
|
|
58
|
+
model.supportsFast
|
|
59
|
+
) {
|
|
60
|
+
payload.service_tier = "priority";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Run provider edits first; the caller always sees and can replace the final payload. */
|
|
65
|
+
export const wrapCliproxyStreams = (
|
|
66
|
+
streams: ProviderStreams,
|
|
67
|
+
settings: PayloadSettings
|
|
68
|
+
): ProviderStreams => {
|
|
69
|
+
const withPayload = <T extends StreamOptions>(
|
|
70
|
+
options: T
|
|
71
|
+
): T & StreamOptions => ({
|
|
72
|
+
...options,
|
|
73
|
+
apiKey: options?.apiKey ?? settings.apiKey,
|
|
74
|
+
onPayload: async (payload, model) => {
|
|
75
|
+
// SAFETY: These wrappers receive request objects from pi-ai's three SDK adapters, not external JSON.
|
|
76
|
+
applyPayloadSettings(payload as RequestPayload, model, options, settings);
|
|
77
|
+
|
|
78
|
+
return (await options?.onPayload?.(payload, model)) ?? payload;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...streams,
|
|
84
|
+
stream: (model, context, options) =>
|
|
85
|
+
streams.stream(model, context, withPayload(options ?? {})),
|
|
86
|
+
streamSimple: (model, context, options) =>
|
|
87
|
+
streams.streamSimple(model, context, withPayload(options ?? {})),
|
|
88
|
+
};
|
|
89
|
+
};
|