The Models page could name a provider's key and little else. Adding an OpenAI-compatible gateway meant opening $DSH_HOME/settings.yaml and knowing the profile shape; correcting a stale context window meant the same. This layer puts both on the page: a card that declares a route pi-ai does not ship — id, endpoint, protocol, key, models — and a model list on the pi-ai editor that can ask the provider what it serves and adopt the answer. It follows the DeepSeek catalog editor that landed in #1050 rather than inventing a second look for the same job. Both editors now share the section shell and heading, the danger-tinted delete, the add-model button, the empty state, the per-row validator that names a bad row by its position, and one K/M capacity vocabulary — 256K and 1M are read and spelled back, while settings.yaml still stores plain token counts. The row type is structurally open like that editor's, so a profile field this card does not edit survives an edit here. Three of that editor's decisions replaced weaker ones this branch had made. Inheritance now reads the composition base rather than the effective value, which would echo an override back the moment a reset dropped it. Validation names the offending row instead of stating a blanket problem. And emptying the list is no longer conflated with handing the catalog back to the adapter — those are separate acts, with separate affordances. The create write carries the revision the card opened at, so a route another tab declared meanwhile is a conflict rather than a silent overwrite of its profile.
87 lines
3.8 KiB
TypeScript
87 lines
3.8 KiB
TypeScript
/**
|
|
* Browser wire client. The plugin selects fixture or HTTP transport, provides
|
|
* the shared API client, and lets the runtime object layer start the stream
|
|
* controller with its sinks.
|
|
*/
|
|
import type { Context } from 'cordis'
|
|
import type { IApiClient } from './api.ts'
|
|
import { ConnectionController, type ConnectionConfig, type ConnectionSinks, type ConnectionState } from './connection.ts'
|
|
import { FixtureApiClient } from './fixture.ts'
|
|
import { WebApiClient } from './web-api-client.ts'
|
|
import { isLoopbackHostname } from '../loopback-hostname.ts'
|
|
|
|
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
|
export type {
|
|
ApiProxy, SessionsApi, SessionSearchItem, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
|
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
|
DirectoryEntry, DirectoryListing,
|
|
ToolCallView, ToolResultView, WorkspaceApi, WorkspaceId, WorkspaceView,
|
|
CommandsApi, CommandDescriptor, SkillsApi, SkillEntry,
|
|
ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning,
|
|
MessageId, ModelReasoningEffort, ModelTarget, QueueAction, QueuedInboxItem, SessionModels,
|
|
SubagentsApi, SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt,
|
|
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
|
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
|
IApiClient, SessionId, SessionEvent, ContentBlock, StreamChunk,
|
|
GoalsApi, GoalRef,
|
|
SettingsApi, SettingsNamespaceView, SettingsPathOpView, SettingsSecretView,
|
|
CredentialsApi, CredentialView, ConfigurableProviderView, DiscoveredModelView, LlmApi,
|
|
} from './api.ts'
|
|
export {
|
|
RpcId,
|
|
AbstractApiClient,
|
|
transportError,
|
|
} from './api.ts'
|
|
|
|
// Connection loop types are public through ConnectionHandle.start; the
|
|
// controller remains package-internal.
|
|
export type { ConnectionConfig, ConnectionSinks, ConnectionState }
|
|
|
|
|
|
/** Required services (none — this is the wire root). */
|
|
export const inject: string[] = []
|
|
|
|
/**
|
|
* The ctx.connection service surface: the api client plus a one-shot
|
|
* controller starter (the runtime plugin supplies sinks when its object layer
|
|
* is ready — connection stays consumer-agnostic).
|
|
*/
|
|
export interface ConnectionHandle {
|
|
/** Shared api client (fixture or real, decided at boot from the page URL). */
|
|
readonly api: IApiClient
|
|
/** Whether the current page authority is loopback; non-browser contexts default to true. */
|
|
readonly isLoopback: boolean
|
|
/**
|
|
* Start the connect/pump/reconnect loop with the consumer's frame sinks.
|
|
* One consumer owns the streams (the runtime object layer); a second call
|
|
* throws.
|
|
* @param sinks - frame/state callbacks.
|
|
* @param config - reconnect/backoff tunables.
|
|
* @returns stop handle for the loop.
|
|
*/
|
|
start(sinks: ConnectionSinks, config?: ConnectionConfig): { stop(): void }
|
|
}
|
|
|
|
/**
|
|
* Client plugin body: pick the api by page mode and provide ctx.connection.
|
|
* @param ctx - client cordis context.
|
|
*/
|
|
export function apply(ctx: Context): void {
|
|
const pageLocation = typeof location === 'undefined' ? undefined : location
|
|
const fixture = pageLocation !== undefined && new URLSearchParams(pageLocation.search).has('fixture')
|
|
const api: IApiClient = fixture ? new FixtureApiClient() : new WebApiClient()
|
|
let started = false
|
|
const handle: ConnectionHandle = {
|
|
api,
|
|
isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname),
|
|
start(sinks, config) {
|
|
if (started) throw new Error('connection: the stream loop is already owned by another consumer')
|
|
started = true
|
|
const controller = new ConnectionController(api, sinks, config ?? {})
|
|
controller.start()
|
|
return { stop: () => { controller.stop() } }
|
|
},
|
|
}
|
|
ctx.provide('connection', handle)
|
|
}
|