evOo-local/packages/adapters/src/executor-model.ts
Ingo Wagner a570e9e71b feat: complete onboarding redesign + major file splits
Onboarding: emoji-first flow with shape/color selection, user preferences,
language setup (9 locales), and bot creation with avatarValue persistence.

executor.ts: split 4928→4159 lines into 8 modules (completion, computer,
effects, model, utils, tools, attachments, types).

pi-runtime.ts: split 1726→561 lines into 2 modules (model-config, tool-utils).

E2E: new onboarding-emoji-first tests + updated completeOnboarding helper.
2026-09-21 22:52:04 +00:00

208 lines
6.9 KiB
TypeScript

import type { AgentModelOAuthCredential } from "@rakazo/adapter-kit";
import type { AgentRunRequest } from "@rakazo/adapter-kit";
import { OPENAI_COMPATIBLE_PROVIDER_ID } from "@rakazo/contracts";
import { resolveDeploymentModel } from "./deployment-model.js";
import { modelIdSupportsImages } from "./model-vision.js";
import { toOAuthCredential } from "./pi-credentials.js";
import {
parseModelSecret,
resolveModelAuth,
secretValuesToRedact,
serializeModelSecret,
} from "./pi-oauth.js";
import type { ExecutorDeps } from "./executor-types.js";
const modelCredentialLocks = new Map<string, Promise<void>>();
/**
* Return a deployment API key when the winning provider matches the
* configured deployment model provider. A provider named by deployment
* settings or a bot override gets no key rather than another vendor's.
*/
export function deploymentKeyFor(
deps: ExecutorDeps,
provider: string,
): string | undefined {
if (!deps.deploymentModelKey) return undefined;
return provider === resolveDeploymentModel().provider
? deps.deploymentModelKey
: undefined;
}
/**
* Resolve model credentials for a given provider/model pair.
*
* When a credential row exists, fetch the secret from the database,
* apply any required OAuth flow, and return the resolved API key plus
* metadata (base URL, reasoning flags, image support, etc.).
*
* When no credential exists, fall back to the deployment key for the
* winning provider.
*/
export async function resolveModelKey(
deps: ExecutorDeps,
userId: string,
spaceId: string,
credential: {
secretId: string;
provider: string;
defaultModel?: string | null;
supportsImages?: boolean;
} | null,
provider: string,
modelId: string,
registerSecrets?: (values: string[]) => void,
): Promise<{
apiKey?: string;
baseUrl?: string;
reasoning?: boolean;
maxTokens?: number;
contextWindow?: number;
thinkingLevel?: AgentRunRequest["model"]["thinkingLevel"];
acceptsImages?: boolean;
maxImagesPerPrompt?: number;
oauth?: AgentModelOAuthCredential;
persistOAuth?: (credential: AgentModelOAuthCredential) => Promise<void>;
redact: string[];
}> {
if (credential) {
return withModelCredentialLock(credential.secretId, async () => {
const row = await deps.prisma.secret.findFirst({
where: { id: credential.secretId, userId, spaceId: null },
});
if (!row)
return { apiKey: deploymentKeyFor(deps, provider), redact: [] };
const plaintext = deps.secretStore.load(row.ciphertext, row.id);
registerSecrets?.(secretValuesToRedact(parseModelSecret(plaintext)));
const persist = async (next: string) => {
const stored = await deps.secretStore.put(
next,
{
operationId: "cred",
traceId: "cred-refresh",
spaceId,
userId,
signal: new AbortController().signal,
},
row.id,
);
await deps.prisma.secret.update({
where: { id: row.id },
data: { ciphertext: stored.ciphertext },
});
};
const resolved = await resolveModelAuth(plaintext, credential.provider, {
persist,
});
const oauth =
resolved.secret.kind === "oauth"
? resolved.secret.credential
: undefined;
const baseUrl =
resolved.secret.kind === "openai_compatible"
? resolved.secret.baseUrl
: undefined;
const acceptsImages =
credential.provider === OPENAI_COMPATIBLE_PROVIDER_ID &&
resolved.secret.kind === "openai_compatible" &&
(modelIdSupportsImages(resolved.secret.visionModelIds, modelId) ||
// Legacy secrets have no per-model list, so keep their existing
// capability scoped to the model saved in the space preference.
(resolved.secret.visionModelIds === undefined &&
credential.supportsImages === true &&
credential.defaultModel?.trim() === modelId.trim()));
return {
apiKey: resolved.apiKey,
baseUrl,
reasoning:
resolved.secret.kind === "openai_compatible"
? resolved.secret.reasoning
: undefined,
maxTokens:
resolved.secret.kind === "openai_compatible"
? resolved.secret.maxTokens
: undefined,
contextWindow:
resolved.secret.kind === "openai_compatible"
? resolved.secret.contextWindow
: undefined,
thinkingLevel:
resolved.secret.kind === "openai_compatible"
? resolved.secret.thinkingLevel
: undefined,
acceptsImages,
maxImagesPerPrompt:
resolved.secret.kind === "openai_compatible"
? resolved.secret.maxImagesPerPrompt
: undefined,
oauth,
persistOAuth: oauth
? async (next) => {
await withModelCredentialLock(credential.secretId, async () => {
const currentRow = await deps.prisma.secret.findFirst({
where: { id: credential.secretId, userId, spaceId: null },
});
if (!currentRow) return;
const current = parseModelSecret(
deps.secretStore.load(
currentRow.ciphertext,
currentRow.id,
),
);
if (current.kind === "oauth") {
const stored = current.credential;
if (stored.expires > next.expires) return;
if (
stored.access === next.access &&
stored.refresh === next.refresh &&
stored.expires === next.expires
) {
return;
}
}
await persist(
serializeModelSecret({
kind: "oauth",
credential: toOAuthCredential(next),
}),
);
});
}
: undefined,
redact: [...secretValuesToRedact(resolved.secret), resolved.apiKey].filter(
(value): value is string => Boolean(value),
),
};
});
}
return { apiKey: deploymentKeyFor(deps, provider), redact: [] };
}
/**
* Serialize access to credential operations for a single secret key.
* Prevents concurrent reads/writes from producing torn state.
*/
export async function withModelCredentialLock<T>(
key: string,
fn: () => Promise<T>,
): Promise<T> {
const previous = modelCredentialLocks.get(key) ?? Promise.resolve();
let release!: () => void;
const current = previous.then(
() =>
new Promise<void>((resolve) => {
release = resolve;
}),
);
modelCredentialLocks.set(key, current);
await previous;
try {
return await fn();
} finally {
release();
if (modelCredentialLocks.get(key) === current)
modelCredentialLocks.delete(key);
}
}