|
@@ -144,7 +144,7 @@ export interface PiAiProviderProfile {
|
|
|
* to answer instead.
|
|
* to answer instead.
|
|
|
*/
|
|
*/
|
|
|
defaultInput?: PiAiModality[]
|
|
defaultInput?: PiAiModality[]
|
|
|
- /** Provider request headers; Harness attribution wins reserved names. */
|
|
|
|
|
|
|
+ /** Provider request headers, validated against Fetch when the profile resolves; Harness attribution wins reserved names. */
|
|
|
headers?: Record<string, string>
|
|
headers?: Record<string, string>
|
|
|
/** Provider-neutral pi-ai reasoning level. */
|
|
/** Provider-neutral pi-ai reasoning level. */
|
|
|
reasoning?: ModelThinkingLevel
|
|
reasoning?: ModelThinkingLevel
|
|
@@ -351,7 +351,7 @@ export const Config: z<Config> = z.object({
|
|
|
* renders and the value an absent section resolves to; wrapping it would break
|
|
* renders and the value an absent section resolves to; wrapping it would break
|
|
|
* both.
|
|
* both.
|
|
|
* @param config - the resolved section to check.
|
|
* @param config - the resolved section to check.
|
|
|
- * @throws Error naming the route and model that cannot be served.
|
|
|
|
|
|
|
+ * @throws Error naming the route and configuration entry that cannot be served.
|
|
|
*/
|
|
*/
|
|
|
export function assertServiceable(config: Config): void {
|
|
export function assertServiceable(config: Config): void {
|
|
|
resolveProfiles(config.providers)
|
|
resolveProfiles(config.providers)
|
|
@@ -375,6 +375,20 @@ function rejectRemovedFields(provider: string, source: PiAiProviderProfile): voi
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** Reject a profile header that Fetch cannot put on a provider request. */
|
|
|
|
|
+function assertValidHeaders(provider: string, headers: Readonly<Record<string, string>> | undefined): void {
|
|
|
|
|
+ for (const [name, value] of Object.entries(headers ?? {})) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ new Headers([[name, value]])
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ throw new Error(
|
|
|
|
|
+ `llm-pi-ai: provider "${provider}" header "${name}" is not valid for Fetch;`
|
|
|
|
|
+ + ' use a valid HTTP field name and a single-line value representable as bytes',
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Validate profiles and return a detached route-keyed map suitable for
|
|
* Validate profiles and return a detached route-keyed map suitable for
|
|
|
* per-request reads. This is the one explicit resolve step, so an omitted dict
|
|
* per-request reads. This is the one explicit resolve step, so an omitted dict
|
|
@@ -400,6 +414,7 @@ export function resolveProfiles(
|
|
|
if (source.displayName !== undefined && source.displayName.length === 0) {
|
|
if (source.displayName !== undefined && source.displayName.length === 0) {
|
|
|
throw new Error(`llm-pi-ai: provider "${provider}" has an empty displayName`)
|
|
throw new Error(`llm-pi-ai: provider "${provider}" has an empty displayName`)
|
|
|
}
|
|
}
|
|
|
|
|
+ assertValidHeaders(provider, source.headers)
|
|
|
const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
|
|
if (!Number.isFinite(streamIdleTimeoutMs)
|
|
if (!Number.isFinite(streamIdleTimeoutMs)
|
|
|
|| streamIdleTimeoutMs <= 0
|
|
|| streamIdleTimeoutMs <= 0
|