feat(agent): per-upstream pi Models with isolated credential stores
- PiUpstreamClient: one Models per upstream holding only its provider; credential store adapter maps to that upstream's secret, so several accounts of one provider coexist; host env is only read for authKind none (ambient cloud creds) - provider catalog from builtinProviders() plus static oauth modes, env fields and subscription terms notes; openai-compatible custom endpoints - fake upstream provider with mock OAuth (browser paste / device approval) - PiAgentRuntime accepts an upstream; thinking level clamped to the model Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b9dbd54e37
commit
743fe2486d
8 files changed
+1085
-5
No files matched your search
@@ -0,0 +1,171 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import {
|
||||
type AssistantMessage,
|
||||
createProvider,
|
||||
type FauxResponseFactory,
|
||||
fauxAssistantMessage,
|
||||
fauxProvider,
|
||||
type OAuthAuth,
|
||||
type OAuthCredential,
|
||||
type Provider,
|
||||
type ProviderAuthInteraction,
|
||||
} from "@earendil-works/pi-ai";
|
||||
|
||||
// provider = "fake" 的上游:faux 模型 + 模拟 OAuth(browser 粘 URL / device 轮询),不联网。
|
||||
// 设置页联调与 OAuth 状态机测试用;行为由 FakeOAuthControl 控制(测试注入,缺省用进程级默认实例)。
|
||||
|
||||
export class FakeOAuthControl {
|
||||
loginCalls = 0;
|
||||
refreshCalls = 0;
|
||||
/** refresh 抛 invalid_grant */
|
||||
failRefresh = false;
|
||||
/** refresh 前等待(并发测试用) */
|
||||
refreshDelayMs = 0;
|
||||
/** 新 token 的有效期 */
|
||||
tokenTtlMs = 3600_000;
|
||||
/** device 模式:多久后自动批准(null = 只能 approveDevice() 批准) */
|
||||
deviceAutoApproveMs: number | null = 3000;
|
||||
/** browser 模式:模拟回调端口被占用(pi 的 Anthropic 会直接 reject) */
|
||||
browserListenError: string | null = null;
|
||||
private approved = false;
|
||||
|
||||
approveDevice(): void {
|
||||
this.approved = true;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
consumeApproval(): boolean {
|
||||
if (!this.approved) return false;
|
||||
this.approved = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_FAKE_OAUTH = new FakeOAuthControl();
|
||||
|
||||
const sleep = (ms: number, signal?: AbortSignal) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const t = setTimeout(resolve, ms);
|
||||
signal?.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
clearTimeout(t);
|
||||
reject(new Error("Login cancelled"));
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
|
||||
function parsePasted(input: string): { code?: string; state?: string } {
|
||||
const v = input.trim();
|
||||
try {
|
||||
const u = new URL(v);
|
||||
return { code: u.searchParams.get("code") ?? undefined, state: u.searchParams.get("state") ?? undefined };
|
||||
} catch {
|
||||
// code#state 或裸 code
|
||||
}
|
||||
if (v.includes("#")) {
|
||||
const [code, state] = v.split("#", 2);
|
||||
return { code: code || undefined, state: state || undefined };
|
||||
}
|
||||
if (v.includes("code=")) {
|
||||
const p = new URLSearchParams(v);
|
||||
return { code: p.get("code") ?? undefined, state: p.get("state") ?? undefined };
|
||||
}
|
||||
return { code: v || undefined };
|
||||
}
|
||||
|
||||
function issue(control: FakeOAuthControl, account: string): OAuthCredential {
|
||||
const n = randomBytes(6).toString("hex");
|
||||
return {
|
||||
type: "oauth",
|
||||
access: `fake-access-${n}`,
|
||||
refresh: `fake-refresh-${n}`,
|
||||
expires: Date.now() + control.tokenTtlMs,
|
||||
account,
|
||||
};
|
||||
}
|
||||
|
||||
export function fakeOAuth(control: FakeOAuthControl): OAuthAuth {
|
||||
return {
|
||||
name: "Fake OAuth",
|
||||
isSubscription: false,
|
||||
async login(interaction: ProviderAuthInteraction) {
|
||||
control.loginCalls += 1;
|
||||
const method = await interaction.prompt({
|
||||
type: "select",
|
||||
message: "Select fake login method:",
|
||||
options: [
|
||||
{ id: "browser", label: "Browser login" },
|
||||
{ id: "device_code", label: "Device code login" },
|
||||
],
|
||||
});
|
||||
if (method === "device_code") {
|
||||
const userCode = `FAKE-${randomBytes(2).toString("hex").toUpperCase()}`;
|
||||
interaction.notify({
|
||||
type: "device_code",
|
||||
userCode,
|
||||
verificationUri: "https://fake-oauth.invalid/device",
|
||||
intervalSeconds: 1,
|
||||
expiresInSeconds: 900,
|
||||
});
|
||||
const started = Date.now();
|
||||
for (;;) {
|
||||
if (control.consumeApproval()) break;
|
||||
if (control.deviceAutoApproveMs !== null && Date.now() - started >= control.deviceAutoApproveMs) break;
|
||||
await sleep(25, interaction.signal);
|
||||
}
|
||||
interaction.notify({ type: "progress", message: "Device authorized" });
|
||||
return issue(control, "fake-device-user");
|
||||
}
|
||||
if (control.browserListenError) throw new Error(control.browserListenError);
|
||||
const state = randomBytes(8).toString("hex");
|
||||
interaction.notify({
|
||||
type: "auth_url",
|
||||
url: `https://fake-oauth.invalid/authorize?state=${state}&redirect_uri=${encodeURIComponent("http://localhost:1455/auth/callback")}`,
|
||||
instructions: "Complete login in your browser, then paste the final redirect URL here.",
|
||||
});
|
||||
const input = await interaction.prompt({
|
||||
type: "manual_code",
|
||||
message: "Paste the authorization code / redirect URL:",
|
||||
placeholder: "http://localhost:1455/auth/callback",
|
||||
});
|
||||
const parsed = parsePasted(input);
|
||||
if (!parsed.code) throw new Error("Missing authorization code");
|
||||
if (parsed.state && parsed.state !== state) throw new Error("OAuth state mismatch");
|
||||
interaction.notify({ type: "progress", message: "Exchanging authorization code for tokens..." });
|
||||
return issue(control, "fake-browser-user");
|
||||
},
|
||||
async refresh(credential, signal) {
|
||||
control.refreshCalls += 1;
|
||||
if (control.refreshDelayMs > 0) await sleep(control.refreshDelayMs, signal);
|
||||
if (control.failRefresh) throw new Error("invalid_grant: refresh token revoked");
|
||||
return issue(control, String(credential.account ?? "fake-user"));
|
||||
},
|
||||
async toAuth(credential) {
|
||||
return { apiKey: credential.access };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** provider id "fake" 的上游 provider:faux 流 + 始终就绪的 apiKey(authKind none)+ 模拟 OAuth */
|
||||
export function fakeUpstreamProvider(control: FakeOAuthControl): Provider {
|
||||
const faux = fauxProvider({
|
||||
provider: "fake",
|
||||
models: [{ id: "fake-model", contextWindow: 64_000, maxTokens: 4096, reasoning: true }],
|
||||
tokensPerSecond: 0,
|
||||
});
|
||||
const factory: FauxResponseFactory = (): AssistantMessage => {
|
||||
faux.appendResponses([factory]);
|
||||
return fauxAssistantMessage("ok");
|
||||
};
|
||||
faux.setResponses([factory]);
|
||||
const base = faux.provider;
|
||||
return createProvider({
|
||||
id: "fake",
|
||||
name: "Fake",
|
||||
auth: { apiKey: { name: "Fake", resolve: async () => ({ auth: {}, source: "fake" }) }, oauth: fakeOAuth(control) },
|
||||
models: base.getModels(),
|
||||
api: { stream: base.stream.bind(base), streamSimple: base.streamSimple.bind(base) } as never,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user