blob: 26fdac40b2c34dd7b5663a0da54bbc5e2710a3a5 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { authorizeConnectionModel, effectiveBaseUrl } from '@maka/core/llm-connections';
import { readRuntimeHostConnectionCatalog } from './catalog-reader.js';
import type { RuntimeHostConnection } from './connection.js';
import { abortable } from './wait-for-ready.js';
type TargetConnection = Pick<RuntimeHostConnection, 'request'>;
export interface HostedExecutionTargetInput {
readonly connection?: {
readonly providerType: import('@maka/core/llm-connections').ProviderType;
readonly apiKey: string;
};
readonly connectionSlug: string;
readonly model: string;
readonly baseUrl: string;
}
export interface ConfiguredHostedExecutionTarget {
readonly connectionId: string;
readonly connectionSlug: string;
}
export async function configureHostedExecutionTarget(
connection: TargetConnection,
input: HostedExecutionTargetInput,
signal?: AbortSignal,
): Promise<ConfiguredHostedExecutionTarget> {
const before = await abortable(() => readRuntimeHostConnectionCatalog(connection), signal);
const target = before.connections.find((candidate) => candidate.slug === input.connectionSlug);
const onboarding = input.connection;
if (onboarding && target && target.providerType !== onboarding.providerType) {
throw new Error('Runtime Host connection provider does not match');
}
if (onboarding) {
const onboardingTarget = target
? { kind: 'existing' as const, connectionId: target.connectionId }
: {
kind: 'create' as const,
providerType: onboarding.providerType,
slug: input.connectionSlug,
name: input.connectionSlug,
};
const saved = await abortable(
() =>
connection.request('connection.onboarding.save', {
target: onboardingTarget,
apiKey: onboarding.apiKey,
baseUrl: input.baseUrl,
enabledModelIds: [input.model],
}),
signal,
);
if (saved.kind !== 'saved') throw new Error('Runtime Host connection onboarding failed');
return {
connectionId: saved.connection.connectionId,
connectionSlug: saved.connection.slug,
};
}
if (!target) throw new Error('Runtime Host connection is unavailable');
const baseUrl = new URL(input.baseUrl).toString();
const enabledModelIds = [...new Set([...target.enabledModelIds, input.model])];
const endpointChanged = canonicalBaseUrl(effectiveBaseUrl(target)) !== baseUrl;
if (endpointChanged || !target.enabled || !target.enabledModelIds.includes(input.model)) {
const updated = await abortable(
() =>
connection.request('connection.catalog.update', {
expected: { connectionId: target.connectionId, revision: target.revision },
changes: {
name: target.name,
baseUrl,
enabled: true,
enabledModelIds,
},
}),
signal,
);
if (updated.kind !== 'committed') {
throw new Error(`Runtime Host connection update was not committed: ${updated.kind}`);
}
}
// Best-effort: a fetch here is how the target picks up wire metadata for a
// model the catalog has not described yet, so it is worth trying. It is not
// worth failing over. A provider with no model-list endpoint refuses the
// operation outright, one whose list lags simply returns the same array, and
// in both cases the user's selection still authorizes the model — the
// admission check below is what decides.
if (endpointChanged || !target.models.some((model) => model.id === input.model)) {
await abortable(
() =>
connection.request('connection.models.fetch', {
connectionId: target.connectionId,
}),
signal,
);
}
const after = await abortable(() => readRuntimeHostConnectionCatalog(connection), signal);
const configured = after.connections.find(
(candidate) => candidate.connectionId === target.connectionId,
);
if (
!configured?.enabled ||
canonicalBaseUrl(effectiveBaseUrl(configured)) !== baseUrl ||
!authorizeConnectionModel(configured, input.model)
) {
throw new Error('Runtime Host did not admit the requested model target');
}
return {
connectionId: configured.connectionId,
connectionSlug: configured.slug,
};
}
function canonicalBaseUrl(value: string): string | undefined {
try {
return new URL(value).toString();
} catch {
return undefined;
}
}