Add provider background recovery lifecycle
This commit is contained in:
1 parent
08364c2571
commit
e5946f25c8
12 files changed
+921
-130
No files matched your search
@@ -1,6 +1,27 @@
|
||||
import { PROVIDER_AUTH_TYPE, type ProviderAuthType } from "../../shared/domain";
|
||||
import { listProviderModels, type ProviderModelEntry } from "../../shared/provider-models";
|
||||
import type { ProviderObservedRateLimitState } from "../../shared/provider-runtime-domain";
|
||||
import {
|
||||
parseAccessToken,
|
||||
parseAccountToken,
|
||||
parseApiKey,
|
||||
parseAzureProfile,
|
||||
parseBedrockProfile,
|
||||
parseVertexProfile,
|
||||
requireAccessTokenProfile,
|
||||
requireAccountTokenProfile,
|
||||
requireApiKeyProfile,
|
||||
requireAzureProfile,
|
||||
requireBedrockProfile,
|
||||
requireVertexProfile,
|
||||
type ProviderAuthProfile,
|
||||
} from "./provider-auth-profiles";
|
||||
import {
|
||||
backgroundUnsupported,
|
||||
retrieveUnsupported,
|
||||
type ProviderBackgroundCreateResult,
|
||||
type ProviderBackgroundRetrieveResult,
|
||||
} from "./provider-background";
|
||||
import {
|
||||
authorizeWithParser,
|
||||
refreshUnsupported,
|
||||
@@ -21,6 +42,7 @@ import {
|
||||
} from "./provider-streaming";
|
||||
|
||||
export { ProviderHttpError, providerRateLimitStateFromError } from "./provider-http";
|
||||
export type { ProviderAuthProfile } from "./provider-auth-profiles";
|
||||
|
||||
export interface ProviderExecutionInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
@@ -36,12 +58,18 @@ export interface ProviderExecutionResult {
|
||||
}
|
||||
|
||||
export type ProviderStreamInput = ProviderExecutionInput;
|
||||
export type ProviderBackgroundCreateInput = ProviderExecutionInput;
|
||||
|
||||
export interface ProviderCancelInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export interface ProviderBackgroundRetrieveInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
responseId: string;
|
||||
}
|
||||
|
||||
export interface ProviderHealthInput {
|
||||
authProfile: ProviderAuthProfile;
|
||||
model: string | null;
|
||||
@@ -77,19 +105,13 @@ export interface ProviderAdapter {
|
||||
checkHealth(input: ProviderHealthInput): Promise<ProviderHealthResult>;
|
||||
streamResponse(input: ProviderStreamInput): Promise<ProviderStreamResult>;
|
||||
cancelResponse(input: ProviderCancelInput): Promise<ProviderCancelResult>;
|
||||
createBackgroundResponse(input: ProviderBackgroundCreateInput): Promise<ProviderBackgroundCreateResult>;
|
||||
retrieveResponse(input: ProviderBackgroundRetrieveInput): Promise<ProviderBackgroundRetrieveResult>;
|
||||
createResponse?: (input: ProviderExecutionInput) => Promise<ProviderExecutionResult>;
|
||||
}
|
||||
|
||||
export type ProviderAdapterRegistry = Record<string, ProviderAdapter>;
|
||||
|
||||
export type ProviderAuthProfile =
|
||||
| { type: "api_key"; apiKey: string }
|
||||
| { type: "access_token"; accessToken: string }
|
||||
| { type: "account_token"; accessToken: string }
|
||||
| { type: "azure_openai"; baseUrl: string; deployment: string; apiKey: string }
|
||||
| { type: "aws_bedrock"; region: string; accessKeyId: string; secretAccessKey: string; sessionToken: string | null }
|
||||
| { type: "vertex_ai"; projectId: string; region: string; serviceAccountJson: string };
|
||||
|
||||
export const providerAdapters: ProviderAdapterRegistry = {
|
||||
openai: {
|
||||
providerKey: "openai",
|
||||
@@ -121,6 +143,40 @@ export const providerAdapters: ProviderAdapterRegistry = {
|
||||
}, {});
|
||||
return { responseId, rawResponseJson: JSON.stringify(response.data), rateLimitState: response.rateLimitState };
|
||||
},
|
||||
async createBackgroundResponse(input) {
|
||||
const profile = requireApiKeyProfile(input.authProfile, "OpenAI");
|
||||
const response = await postJson("https://api.openai.com/v1/responses", {
|
||||
authorization: `Bearer ${profile.apiKey}`,
|
||||
}, {
|
||||
model: input.model,
|
||||
input: input.prompt,
|
||||
background: true,
|
||||
store: true,
|
||||
...(input.instructions ? { instructions: input.instructions } : {}),
|
||||
});
|
||||
const data = response.data;
|
||||
return {
|
||||
responseId: requireStringField(data, "id", "OpenAI response id"),
|
||||
status: requireStringField(data, "status", "OpenAI response status"),
|
||||
rawResponseJson: JSON.stringify(data),
|
||||
rateLimitState: response.rateLimitState,
|
||||
};
|
||||
},
|
||||
async retrieveResponse(input) {
|
||||
const profile = requireApiKeyProfile(input.authProfile, "OpenAI");
|
||||
const responseId = requireResponseId(input.responseId);
|
||||
const response = await getJson(`https://api.openai.com/v1/responses/${encodeURIComponent(responseId)}`, {
|
||||
authorization: `Bearer ${profile.apiKey}`,
|
||||
});
|
||||
const data = response.data;
|
||||
return {
|
||||
responseId: requireStringField(data, "id", "OpenAI response id"),
|
||||
status: requireStringField(data, "status", "OpenAI response status"),
|
||||
text: optionalText(collectText(data.output_text ?? data.output)),
|
||||
rawResponseJson: JSON.stringify(data),
|
||||
rateLimitState: response.rateLimitState,
|
||||
};
|
||||
},
|
||||
async createResponse(input) {
|
||||
const profile = requireApiKeyProfile(input.authProfile, "OpenAI");
|
||||
const response = await postJson("https://api.openai.com/v1/responses", {
|
||||
@@ -167,6 +223,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
|
||||
return { events: mapAnthropicMessageStream(response.messages), rateLimitState: response.rateLimitState };
|
||||
},
|
||||
cancelResponse: cancelUnsupported("Anthropic"),
|
||||
createBackgroundResponse: backgroundUnsupported("Anthropic"),
|
||||
retrieveResponse: retrieveUnsupported("Anthropic"),
|
||||
async createResponse(input) {
|
||||
const profile = requireApiKeyProfile(input.authProfile, "Anthropic");
|
||||
const response = await postJson("https://api.anthropic.com/v1/messages", {
|
||||
@@ -209,6 +267,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
|
||||
getRateLimitState: providerRateLimitUnknown,
|
||||
streamResponse: streamUnsupported("Azure OpenAI"),
|
||||
cancelResponse: cancelUnsupported("Azure OpenAI"),
|
||||
createBackgroundResponse: backgroundUnsupported("Azure OpenAI"),
|
||||
retrieveResponse: retrieveUnsupported("Azure OpenAI"),
|
||||
async checkHealth(input) {
|
||||
const profile = requireAzureProfile(input.authProfile);
|
||||
const response = await getJson(`${profile.baseUrl}/openai/deployments?api-version=2024-10-21`, { "api-key": profile.apiKey });
|
||||
@@ -226,6 +286,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
|
||||
getRateLimitState: providerRateLimitUnknown,
|
||||
streamResponse: streamUnsupported("AWS Bedrock"),
|
||||
cancelResponse: cancelUnsupported("AWS Bedrock"),
|
||||
createBackgroundResponse: backgroundUnsupported("AWS Bedrock"),
|
||||
retrieveResponse: retrieveUnsupported("AWS Bedrock"),
|
||||
checkHealth: async (input) => {
|
||||
requireBedrockProfile(input.authProfile);
|
||||
return healthy(null);
|
||||
@@ -242,6 +304,8 @@ export const providerAdapters: ProviderAdapterRegistry = {
|
||||
getRateLimitState: providerRateLimitUnknown,
|
||||
streamResponse: streamUnsupported("Vertex AI"),
|
||||
cancelResponse: cancelUnsupported("Vertex AI"),
|
||||
createBackgroundResponse: backgroundUnsupported("Vertex AI"),
|
||||
retrieveResponse: retrieveUnsupported("Vertex AI"),
|
||||
checkHealth: async (input) => {
|
||||
requireVertexProfile(input.authProfile);
|
||||
return healthy(null);
|
||||
@@ -267,6 +331,17 @@ function requireText(parts: string[], providerKey: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
function optionalText(parts: string[]): string | null {
|
||||
const text = parts.map((part) => part.trim()).filter(Boolean).join("\n");
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function requireStringField(record: JsonRecord, key: string, label: string): string {
|
||||
const value = record[key];
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required.`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function openAiCompatibleAdapter({
|
||||
providerKey,
|
||||
label,
|
||||
@@ -301,6 +376,8 @@ function openAiCompatibleAdapter({
|
||||
return { events: mapOpenAiChatCompletionStream(response.messages), rateLimitState: response.rateLimitState };
|
||||
},
|
||||
cancelResponse: cancelUnsupported(label),
|
||||
createBackgroundResponse: backgroundUnsupported(label),
|
||||
retrieveResponse: retrieveUnsupported(label),
|
||||
async createResponse(input) {
|
||||
const profile = requireApiKeyProfile(input.authProfile, label);
|
||||
const messages = responseMessages(input.instructions, input.prompt);
|
||||
@@ -344,6 +421,8 @@ function browserTokenAdapter(providerKey: string, authType: ProviderAuthType): P
|
||||
getRateLimitState: providerRateLimitUnknown,
|
||||
streamResponse: streamUnsupported(providerKey),
|
||||
cancelResponse: cancelUnsupported(providerKey),
|
||||
createBackgroundResponse: backgroundUnsupported(providerKey),
|
||||
retrieveResponse: retrieveUnsupported(providerKey),
|
||||
checkHealth: async (input) => {
|
||||
if (authType === PROVIDER_AUTH_TYPE.ACCOUNT_SUBSCRIPTION) requireAccountTokenProfile(input.authProfile);
|
||||
else requireAccessTokenProfile(input.authProfile);
|
||||
@@ -352,118 +431,6 @@ function browserTokenAdapter(providerKey: string, authType: ProviderAuthType): P
|
||||
};
|
||||
}
|
||||
|
||||
function parseApiKey(secret: string): ProviderAuthProfile {
|
||||
return { type: "api_key", apiKey: nonEmptySecret(secret, "API key") };
|
||||
}
|
||||
|
||||
function parseAccessToken(secret: string): ProviderAuthProfile {
|
||||
return { type: "access_token", accessToken: nonEmptySecret(secret, "Access token") };
|
||||
}
|
||||
|
||||
function parseAccountToken(secret: string): ProviderAuthProfile {
|
||||
return { type: "account_token", accessToken: nonEmptySecret(secret, "Account token") };
|
||||
}
|
||||
|
||||
function parseAzureProfile(secret: string): ProviderAuthProfile {
|
||||
const parsed = parseCloudSecret(secret);
|
||||
const baseUrl = requiredString(parsed.baseUrl, "Azure base URL").replace(/\/+$/, "");
|
||||
const deployment = requiredString(parsed.deployment, "Azure deployment");
|
||||
return { type: "azure_openai", baseUrl: normalizedHttpsUrl(baseUrl, "Azure base URL"), deployment, apiKey: requiredString(parsed.secret, "Azure API key") };
|
||||
}
|
||||
|
||||
function parseBedrockProfile(secret: string): ProviderAuthProfile {
|
||||
const parsed = parseCloudSecret(secret);
|
||||
const credential = parseCredentialJson(requiredString(parsed.secret, "AWS credential"));
|
||||
return {
|
||||
type: "aws_bedrock",
|
||||
region: requiredString(parsed.region, "AWS region"),
|
||||
accessKeyId: requiredString(credential.accessKeyId, "AWS access key id"),
|
||||
secretAccessKey: requiredString(credential.secretAccessKey, "AWS secret access key"),
|
||||
sessionToken: typeof credential.sessionToken === "string" && credential.sessionToken.trim() ? credential.sessionToken.trim() : null,
|
||||
};
|
||||
}
|
||||
|
||||
function parseVertexProfile(secret: string): ProviderAuthProfile {
|
||||
const parsed = parseCloudSecret(secret);
|
||||
const serviceAccountJson = requiredString(parsed.secret, "Vertex service account JSON");
|
||||
parseCredentialJson(serviceAccountJson);
|
||||
return {
|
||||
type: "vertex_ai",
|
||||
projectId: requiredString(parsed.projectId, "Vertex project id"),
|
||||
region: requiredString(parsed.region, "Vertex region"),
|
||||
serviceAccountJson,
|
||||
};
|
||||
}
|
||||
|
||||
function parseCloudSecret(secret: string): JsonRecord {
|
||||
try {
|
||||
const parsed = JSON.parse(secret) as unknown;
|
||||
if (!isRecord(parsed)) throw new Error("Cloud credential profile is invalid.");
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) throw new Error("Cloud credential profile must be JSON.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function parseCredentialJson(value: string): JsonRecord {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
if (!isRecord(parsed)) throw new Error("Credential JSON is invalid.");
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) throw new Error("Credential must be JSON.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function requiredString(value: unknown, label: string): string {
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required.`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function nonEmptySecret(secret: string, label: string): string {
|
||||
const value = secret.trim();
|
||||
if (!value) throw new Error(`${label} is required.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizedHttpsUrl(value: string, label: string): string {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "https:") throw new Error(`${label} must use HTTPS.`);
|
||||
return url.toString().replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function requireApiKeyProfile(profile: ProviderAuthProfile, label: string): Extract<ProviderAuthProfile, { type: "api_key" }> {
|
||||
if (profile.type !== "api_key") throw new Error(`${label} API key profile is required.`);
|
||||
return profile;
|
||||
}
|
||||
|
||||
function requireAccessTokenProfile(profile: ProviderAuthProfile): Extract<ProviderAuthProfile, { type: "access_token" }> {
|
||||
if (profile.type !== "access_token") throw new Error("Access token profile is required.");
|
||||
return profile;
|
||||
}
|
||||
|
||||
function requireAccountTokenProfile(profile: ProviderAuthProfile): Extract<ProviderAuthProfile, { type: "account_token" }> {
|
||||
if (profile.type !== "account_token") throw new Error("Account token profile is required.");
|
||||
return profile;
|
||||
}
|
||||
|
||||
function requireAzureProfile(profile: ProviderAuthProfile): Extract<ProviderAuthProfile, { type: "azure_openai" }> {
|
||||
if (profile.type !== "azure_openai") throw new Error("Azure OpenAI profile is required.");
|
||||
return profile;
|
||||
}
|
||||
|
||||
function requireBedrockProfile(profile: ProviderAuthProfile): Extract<ProviderAuthProfile, { type: "aws_bedrock" }> {
|
||||
if (profile.type !== "aws_bedrock") throw new Error("AWS Bedrock profile is required.");
|
||||
return profile;
|
||||
}
|
||||
|
||||
function requireVertexProfile(profile: ProviderAuthProfile): Extract<ProviderAuthProfile, { type: "vertex_ai" }> {
|
||||
if (profile.type !== "vertex_ai") throw new Error("Vertex AI profile is required.");
|
||||
return profile;
|
||||
}
|
||||
|
||||
function requireResponseId(value: string): string {
|
||||
const responseId = value.trim();
|
||||
if (!responseId) throw new Error("Provider response id is required.");
|
||||
|
||||
Reference in new issue
Block a user