diff --git a/apps/mobile/src/features/threads/ThreadSettingsRows.shared.tsx b/apps/mobile/src/features/threads/ThreadSettingsRows.shared.tsx index 40841697892a9..350f916070030 100644 --- a/apps/mobile/src/features/threads/ThreadSettingsRows.shared.tsx +++ b/apps/mobile/src/features/threads/ThreadSettingsRows.shared.tsx @@ -80,6 +80,11 @@ export function ModelRowContent( Legacy ) : null} + {props.option.isGateway ? ( + + LiteLLM + + ) : null} {props.option.isHidden ? ( Hidden ) : null} diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index 7771c738042d3..616411c9a6a48 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -40,6 +40,8 @@ export type ModelOption = { }; readonly isDefault: boolean; readonly isLegacy: boolean; + /** Served by a LiteLLM gateway rather than the harness's own catalog. */ + readonly isGateway?: boolean; readonly isUnavailable?: boolean; readonly isHidden?: boolean; readonly canHide?: boolean; @@ -381,6 +383,7 @@ export function buildModelOptions( canHide: !model.isCustom, isDefault: model.isDefault === true, isLegacy: model.isLegacy === true, + ...(model.gateway ? { isGateway: true } : {}), ...(updateRequired ? { providerUpdateRequired: updateRequired } : {}), capabilities: model.capabilities, selection: normalizeSelectionOptions( @@ -444,6 +447,7 @@ export function buildModelOptions( providerBadge: providerBadgeFor(fallbackModelSelection.instanceId), isDefault: false, isLegacy: model?.isLegacy === true, + ...(model?.gateway ? { isGateway: true } : {}), ...(isModelSelectionUnavailable(config, fallbackModelSelection, projectId) || (provider !== undefined && !isProviderEnabledForProject(config, provider, projectId)) || provider?.updateRequiredModels?.some((gated) => gated.slug === fallbackModelSelection.model) diff --git a/apps/server/src/orchestration-v2/ProviderAuthRetry.ts b/apps/server/src/orchestration-v2/ProviderAuthRetry.ts index 47e6cfa5bb92e..90acf1655fb08 100644 --- a/apps/server/src/orchestration-v2/ProviderAuthRetry.ts +++ b/apps/server/src/orchestration-v2/ProviderAuthRetry.ts @@ -17,7 +17,7 @@ import * as ThreadManagementService from "./ThreadManagementService.ts"; const RETRY_MESSAGE_PREFIX = "message:auth-retry:"; const AUTH_REJECTION_CODES = new Set(["api_error_401", "api_error_403"]); -// Codex, OpenCode and Pi only pass the gateway's text through. LiteLLM answers an +// Codex and OpenCode only pass the gateway's text through. LiteLLM answers an // expired or revoked key with "Authentication Error, ..." and HTTP 401. // A bare 401 or "unauthorized" is too common in tool output to count. const AUTH_REJECTION_TEXT = diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 3ee0c42cce206..348189922a6e0 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -21,7 +21,7 @@ import { type LiteLLMLauncherEnv, } from "@t3tools/provider-core/server/litellm"; -import { LITELLM_CLAUDE, LITELLM_CODEX, LITELLM_OPENCODE, LITELLM_PI } from "./liteLLMLaunchers.ts"; +import { LITELLM_CLAUDE, LITELLM_CODEX, LITELLM_OPENCODE } from "./liteLLMLaunchers.ts"; export type BuiltInDriversEnv = | AcpRegistryDriverEnv @@ -46,7 +46,7 @@ export const BUILT_IN_DRIVERS: ReadonlyArray = { gatewayModelSlug: claudeGatewayModelSlug, }; +const OPENCODE_GATEWAY_PREFIX = "litellm/"; + export const LITELLM_OPENCODE: LiteLLMLauncherSpec = { harness: "opencode", configure: (config, launch) => config.serverUrl ? Effect.fail("Clear Server URL: LiteLLM can only route an OpenCode server T3 starts.") : Effect.succeed({ ...config, binaryPath: launch.binaryPath }), -}; - -export const LITELLM_PI: LiteLLMLauncherSpec = { - harness: "pi", - configure: (config, launch) => Effect.succeed({ ...config, binaryPath: launch.binaryPath }), - homeEnvironment: (homeDir) => ({ PI_CODING_AGENT_DIR: homeDir }), + // `lite opencode` adds the gateway as the `litellm` provider; other providers stay direct. + listedModelGatewayId: (slug) => + slug.startsWith(OPENCODE_GATEWAY_PREFIX) + ? slug.slice(OPENCODE_GATEWAY_PREFIX.length) + : undefined, }; diff --git a/apps/web/src/components/chat/ModelListRow.tsx b/apps/web/src/components/chat/ModelListRow.tsx index 69da770f1f84b..4c584f20d3b70 100644 --- a/apps/web/src/components/chat/ModelListRow.tsx +++ b/apps/web/src/components/chat/ModelListRow.tsx @@ -29,6 +29,8 @@ export const ModelListRow = memo(function ModelListRow(props: { /** Marks this instance apart from another of the same provider. */ showInstanceBadge: boolean; showNewBadge?: boolean; + /** Served by a LiteLLM gateway rather than the harness's own catalog. */ + showGatewayBadge?: boolean; unavailable?: boolean; jumpLabel?: string | null; disabledReason?: string | null; @@ -93,6 +95,11 @@ export const ModelListRow = memo(function ModelListRow(props: { New ) : null} + {props.showGatewayBadge ? ( + + LiteLLM + + ) : null} {props.unavailable ? ( Unavailable diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index f040df764cda9..294c6dda725ea 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -79,6 +79,7 @@ type ModelPickerItem = { acpRegistryIconUrl?: string | undefined; continuationGroupKey?: string | undefined; isLegacy?: boolean | undefined; + gateway?: "litellm" | undefined; isUnavailable?: boolean | undefined; }; @@ -567,6 +568,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { ...(model.subProvider ? { subProvider: model.subProvider } : {}), ...(model.badge ? { badge: model.badge } : {}), ...(model.isLegacy ? { isLegacy: true } : {}), + ...(model.gateway ? { gateway: model.gateway } : {}), ...(model.isUnavailable ? { isUnavailable: true } : {}), instanceId, driverKind: entry.driverKind, @@ -1056,6 +1058,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { favoriteStarOnHover={selectedInstanceId === "favorites" && !isSearching} showInstanceBadge={badgedInstanceIds.has(model.instanceId)} showNewBadge={model.badge === "new"} + showGatewayBadge={model.gateway === "litellm"} unavailable={model.isUnavailable === true} jumpLabel={modelJumpLabelByKey.get(modelKey) ?? null} disabledReason={disabledReason} diff --git a/apps/web/src/components/chat/providerIconUtils.ts b/apps/web/src/components/chat/providerIconUtils.ts index 565dc25c05f1d..31e325b389642 100644 --- a/apps/web/src/components/chat/providerIconUtils.ts +++ b/apps/web/src/components/chat/providerIconUtils.ts @@ -10,6 +10,7 @@ export type ModelEsque = { isDefault?: boolean | undefined; badge?: "new" | undefined; isLegacy?: boolean | undefined; + gateway?: "litellm" | undefined; isUnavailable?: boolean | undefined; }; diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.logic.ts b/apps/web/src/components/settings/AddProviderInstanceDialog.logic.ts index 2324e31c848b3..ad5264562d246 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.logic.ts +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.logic.ts @@ -27,7 +27,6 @@ export const LITELLM_LAUNCHER_DRIVERS: ReadonlySet = new Set([ "claudeAgent", "codex", "opencode", - "pi", ]); /** The suggested label follows the launcher until the user types their own. */ diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index ab5be4f15fc2e..0f4990eba7554 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -1287,7 +1287,7 @@ export function ProviderInstanceCard({ } /> - {LITELLM_LAUNCHER_DRIVERS.has(instance.driver) ? ( + {LITELLM_LAUNCHER_DRIVERS.has(instance.driver) || instance.launcher === "litellm" ? ( diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx index 3e39d6ccfc12b..6e576ba44abd8 100644 --- a/apps/web/src/components/settings/ProviderModelsSection.tsx +++ b/apps/web/src/components/settings/ProviderModelsSection.tsx @@ -532,6 +532,9 @@ function ProviderModelRow({ {model.isCustom ? ( custom ) : null} + {model.gateway === "litellm" ? ( + LiteLLM + ) : null} {capLabels.length > 0 ? ( diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 96de6feb60ff7..3644c044a82b0 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -70,6 +70,7 @@ export interface AppModelOption { isCustom: boolean; isDefault?: boolean; isLegacy?: boolean; + gateway?: "litellm"; isUnavailable?: boolean; } @@ -80,7 +81,7 @@ const LITELLM_REQUEST_TAG_DRIVERS: ReadonlySet = new Set([ "opencode", ]); -/** LiteLLM tags reach the gateway through Claude, Codex and OpenCode; Pi has no tag channel. */ +/** LiteLLM tags reach the gateway through Claude, Codex and OpenCode. */ export function supportsLiteLLMRequestTags( settings: Pick, instanceId: ProviderInstanceId, @@ -150,6 +151,7 @@ function toAppModelOption( if (model.badge) option.badge = model.badge; if (model.isDefault) option.isDefault = true; if (model.isLegacy) option.isLegacy = true; + if (model.gateway) option.gateway = model.gateway; return option; } diff --git a/docs/user/providers-litellm.md b/docs/user/providers-litellm.md index d8c6cb8fc9fd1..5d83395dcfb37 100644 --- a/docs/user/providers-litellm.md +++ b/docs/user/providers-litellm.md @@ -1,6 +1,6 @@ # LiteLLM -T3 Code can run Claude, Codex, OpenCode, and Pi through a [LiteLLM](https://docs.litellm.ai) +T3 Code can run Claude, Codex, and OpenCode through a [LiteLLM](https://docs.litellm.ai) gateway with the LiteLLM CLI (`lite`). Each harness keeps working as usual, but it signs in with your gateway's SSO and uses the gateway's models. This lets you, for example, use a GPT model on the Claude harness. @@ -17,7 +17,7 @@ GPT model on the Claude harness. 2. Set `LITELLM_PROXY_URL` to your gateway URL in your shell profile, for example `export LITELLM_PROXY_URL=https://litellm.example.com`. The desktop app reads it from your login shell. You can also set it in a provider instance's **Environment variables**. -3. In **Settings > Providers**, choose **Add provider**, pick Claude, Codex, OpenCode, or Pi, +3. In **Settings > Providers**, choose **Add provider**, pick Claude, Codex, or OpenCode, and turn on **Launch through LiteLLM**. The suggested label is, for example, **LiteLLM Claude**, and that is the name the model picker shows. To route an existing provider instead, such as the built-in Codex or Claude, open it and turn on @@ -42,8 +42,12 @@ full URL and paste it in T3 Code when it asks. ## Models - **LiteLLM Claude** lists every model the gateway offers. -- **LiteLLM Codex**, **LiteLLM OpenCode**, and **LiteLLM Pi** list the models that `lite` - syncs into those harnesses. +- **LiteLLM Codex** and **LiteLLM OpenCode** list the models that `lite` syncs into those + harnesses. + +In the model picker and in **Settings > Providers**, a **LiteLLM** label marks each model the +gateway serves. A model without the label comes from the harness's own list. LiteLLM OpenCode +sends those models to their usual provider, not to the gateway. T3 Code checks the gateway's model list at most once a minute, and **Refresh** in **Settings > Providers** checks it now. If the gateway removes the model a thread uses, the @@ -71,13 +75,11 @@ commit messages, and other text generation do not carry tags. - Threads that a thread starts, such as delegated tasks, forks, and threads an agent creates, copy its tags. A scheduled task keeps the tags you set on it. -LiteLLM Pi does not send request tags yet. - ## Limits -- LiteLLM Codex and LiteLLM Pi use their own config directory, so they do not read your - usual Codex or Pi config, skills, or extensions. A Codex provider you switch to LiteLLM - might not resume threads it started before the switch. +- LiteLLM Codex uses its own config directory, so it does not read your usual Codex config or + skills. A Codex provider you switch to LiteLLM might not resume threads it started before + the switch. - When the selected text generation model is not on the gateway, commit messages, titles, and branch names use the first gateway model. - LiteLLM Codex does not accept `-c` or `--config` in **Launch arguments**, because Codex diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index e027930c116ba..2b2c3c853521d 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -83,6 +83,8 @@ export const ServerProviderModel = Schema.Struct({ isCustom: Schema.Boolean, isDefault: Schema.optional(Schema.Boolean), isLegacy: Schema.optional(Schema.Boolean), + /** Set when a LiteLLM gateway serves the model, as opposed to the harness's own catalog. */ + gateway: Schema.optional(Schema.Literal("litellm")), capabilities: Schema.NullOr(ModelCapabilities), }); export type ServerProviderModel = typeof ServerProviderModel.Type; diff --git a/packages/provider-core/src/server/litellm.test.ts b/packages/provider-core/src/server/litellm.test.ts index 6a33e3bf99928..e56f03abfba17 100644 --- a/packages/provider-core/src/server/litellm.test.ts +++ b/packages/provider-core/src/server/litellm.test.ts @@ -35,6 +35,7 @@ import { liteLLMSignedOutMessage, parseLiteLLMModelIds, type LiteLLMHarness, + type LiteLLMStatus, withLiteLLMLauncher, } from "./litellm.ts"; import type { @@ -92,13 +93,13 @@ describe("applyLiteLLMStatus", () => { (id) => id.replace(/^bedrock\//, ""), ); assert.deepEqual( - result.models.map((model) => [model.slug, model.name, model.isCustom]), + result.models.map((model) => [model.slug, model.name, model.isCustom, model.gateway]), [ - ["gpt-5", "gpt-5", false], - ["claude-sonnet-4-5", "Claude Sonnet 4.5", false], - ["bedrock/sonnet-4.5", "bedrock/sonnet-4.5", false], - ["claude sonnet 4.5", "claude sonnet 4.5", false], - ["my-alias", "My alias", true], + ["gpt-5", "gpt-5", false, "litellm"], + ["claude-sonnet-4-5", "Claude Sonnet 4.5", false, "litellm"], + ["bedrock/sonnet-4.5", "bedrock/sonnet-4.5", false, "litellm"], + ["claude sonnet 4.5", "claude sonnet 4.5", false, "litellm"], + ["my-alias", "My alias", true, undefined], ], ); assert.strictEqual(result.models[0]?.capabilities, capabilities); @@ -106,6 +107,30 @@ describe("applyLiteLLMStatus", () => { assert.strictEqual(result.models[3]?.capabilities, sonnetCapabilities); }); + it("marks only the harness-listed models the gateway serves", () => { + const listed = (slug: string) => ({ slug, name: slug, isCustom: false, capabilities: null }); + const harnessSnapshot = snapshot({ + models: [listed("litellm/gpt-5"), listed("openai/gpt-5"), listed("litellm/retired")], + }); + const gatewayId = (slug: string) => + slug.startsWith("litellm/") ? slug.slice("litellm/".length) : undefined; + const marks = (status: LiteLLMStatus) => + applyLiteLLMStatus(harnessSnapshot, status, undefined, undefined, gatewayId).models.map( + (model) => [model.slug, model.gateway], + ); + assert.deepEqual(marks({ auth: { status: "authenticated" }, models: ["gpt-5"] }), [ + ["litellm/gpt-5", "litellm"], + ["openai/gpt-5", undefined], + ["litellm/retired", undefined], + ]); + // Without the gateway's list, no model can claim to be served by it. + assert.deepEqual(marks({ auth: { status: "unauthenticated", message: "Signed out." } }), [ + ["litellm/gpt-5", undefined], + ["openai/gpt-5", undefined], + ["litellm/retired", undefined], + ]); + }); + it("reports the gateway sign-in instead of the harness's own account", () => { const signedOut = applyLiteLLMStatus( snapshot(), @@ -326,7 +351,6 @@ const createLiteLLMInstance = ( harness: options.harness ?? "claude", configure: (config, launch) => Effect.succeed({ ...config, binaryPath: launch.binaryPath, homePath: launch.homeDir }), - homeEnvironment: (homeDir) => ({ FAKE_HOME: homeDir }), gatewayModelCapabilities: capabilities, servesModel: (model, requested) => model.slug === `my-${requested}`, }); @@ -387,10 +411,6 @@ describe.skipIf(windowsHost)("withLiteLLMLauncher", () => { shim, NodePath.join(fixture.dir, "state", "litellm", instanceId, "bin", "claude"), ); - assert.deepEqual( - input.environment.find((variable) => variable.name === "FAKE_HOME")?.value, - input.config.homePath, - ); const run = (args: ReadonlyArray) => spawnAndCollect(shim, ChildProcess.make(shim, [...args], { env: fixture.environment })); @@ -603,8 +623,6 @@ describe.skipIf(windowsHost)("withLiteLLMLauncher", () => { "tool:company,project:billing", ]); assert.deepEqual(yield* tagsWith({ mode: "off", custom: "" }), ["", "project:billing"]); - const pi = yield* createLiteLLMInstance(fixture, [], { harness: "pi" }); - assert.isUndefined(pi.orchestrationAdapter.launchTags); }).pipe(Effect.scoped, Effect.provide(testServices)), ); diff --git a/packages/provider-core/src/server/litellm.ts b/packages/provider-core/src/server/litellm.ts index fd9a673ffa6fb..47b4da4820a0f 100644 --- a/packages/provider-core/src/server/litellm.ts +++ b/packages/provider-core/src/server/litellm.ts @@ -1,5 +1,5 @@ /** - * Runs an existing harness (Claude, Codex, Pi, OpenCode) through the LiteLLM CLI + * Runs an existing harness (Claude, Codex, OpenCode) through the LiteLLM CLI * (`lite `), which signs in with SSO, routes the harness at the gateway * and syncs the gateway's models into the harness. T3 keeps no LiteLLM secrets: * `lite` owns the key, its refresh, and the gateway URL (`LITELLM_PROXY_URL`). @@ -45,9 +45,8 @@ import * as ProviderHost from "./ProviderHost.ts"; import type { ManagedServerProvider } from "./snapshot.ts"; import { spawnAndCollect } from "./snapshotProbe.ts"; -export type LiteLLMHarness = "claude" | "codex" | "pi" | "opencode"; +export type LiteLLMHarness = "claude" | "codex" | "opencode"; -/** `lite pi` first shipped in 1.102.0; `--pkce` login in 1.99.0. */ const MINIMUM_LITELLM_CLI_VERSION = "1.102.0"; const INSTALL_HINT = "Install the LiteLLM CLI with `uv tool install 'litellm[cli]'`."; const STATUS_TTL_MS = 60_000; @@ -129,8 +128,6 @@ const requestTagShimLines = (harness: LiteLLMHarness): ReadonlyArray => return [ `if ${TAGS_SET} && [ -z "\${OPENCODE_CONFIG-}" ]; then export OPENCODE_CONFIG="$(dirname "$0")/${OPENCODE_TAGS_CONFIG}"; fi`, ]; - case "pi": - return []; } }; @@ -225,6 +222,21 @@ const mergeGatewayModels = ( return [...gateway, ...current.filter((model) => model.isCustom && !listed.has(model.slug))]; }; +const markGatewayModels = ( + models: ReadonlyArray, + gatewayIds: ReadonlyArray | undefined, + listedModelGatewayId: (slug: string) => string | undefined, +): ReadonlyArray => { + if (gatewayIds === undefined) return models; + const served = new Set(gatewayIds); + return models.map((model) => { + const gatewayId = listedModelGatewayId(model.slug); + return gatewayId !== undefined && served.has(gatewayId) + ? { ...model, gateway: "litellm" as const } + : model; + }); +}; + /** * `gatewayModelCapabilities` is `undefined` for harnesses that list the models * `lite` syncs into them; otherwise the gateway's list replaces the catalog. @@ -234,23 +246,23 @@ export const applyLiteLLMStatus = ( status: LiteLLMStatus, gatewayModelCapabilities: ModelCapabilities | null | undefined, gatewayModelSlug: (gatewayId: string) => string = (gatewayId) => gatewayId, + listedModelGatewayId: (slug: string) => string | undefined = (slug) => slug, ): ServerProvider => { const authenticated = status.auth.status === "authenticated"; const ownsCatalog = gatewayModelCapabilities !== undefined; + const models = ownsCatalog + ? mergeGatewayModels( + snapshot.models, + status.models ?? [], + gatewayModelCapabilities, + gatewayModelSlug, + ) + : snapshot.models; const base: ServerProvider = { ...snapshot, setup: { ...snapshot.setup, canAuthenticate: true, canInstall: false }, auth: { status: authenticated ? "authenticated" : "unauthenticated", label: "LiteLLM SSO" }, - ...(ownsCatalog - ? { - models: mergeGatewayModels( - snapshot.models, - status.models ?? [], - gatewayModelCapabilities, - gatewayModelSlug, - ), - } - : {}), + models: markGatewayModels(models, status.models, listedModelGatewayId), }; if (status.auth.status !== "authenticated" || !snapshot.enabled) return status.auth.status === "authenticated" || !snapshot.enabled @@ -318,12 +330,12 @@ export interface LiteLLMLauncherSpec { readonly environment: NodeJS.ProcessEnv; }, ) => Effect.Effect; - /** Environment that places the harness home; user values win. */ - readonly homeEnvironment?: (homeDir: string) => Record; /** Set when T3 owns the model catalog (Claude); other harnesses list what `lite` syncs. */ readonly gatewayModelCapabilities?: ModelCapabilities | null; /** Maps a gateway model ID to a catalog slug or alias so it keeps catalog capabilities. */ readonly gatewayModelSlug?: (gatewayId: string) => string; + /** The gateway model ID behind a listed slug, if any; absent means the slug is the ID. */ + readonly listedModelGatewayId?: (slug: string) => string | undefined; /** Aliases the harness resolves to a listed model; exact slugs always count. */ readonly servesModel?: (model: ServerProviderModel, requested: string) => boolean; } @@ -527,14 +539,7 @@ export const withLiteLLMLauncher = ( ); }).pipe(Effect.mapError((cause) => fail("Could not write the LiteLLM launcher.", cause))); - const userEnvironment = new Set(input.environment.map((variable) => variable.name)); - const environment = [ - ...input.environment, - ...Object.entries(spec.homeEnvironment?.(homeDir) ?? {}) - .filter(([name]) => !userEnvironment.has(name)) - .map(([name, value]) => ({ name, value, sensitive: false })), - ]; - const processEnv = yield* mergeProviderInstanceEnvironment(environment); + const processEnv = yield* mergeProviderInstanceEnvironment(input.environment); const config = yield* spec .configure(input.config, { binaryPath, homeDir, environment: processEnv }) .pipe(Effect.mapError((detail) => fail(detail))); @@ -661,10 +666,16 @@ export const withLiteLLMLauncher = ( }; }); - const instance = yield* driver.create({ ...input, config, environment, launchGeneration }); + const instance = yield* driver.create({ ...input, config, launchGeneration }); yield* Deferred.succeed(innerSnapshot, instance.snapshot); const snapshot = wrapSnapshot(instance.snapshot, status, freshStatus, (current, liteStatus) => - applyLiteLLMStatus(current, liteStatus, spec.gatewayModelCapabilities, spec.gatewayModelSlug), + applyLiteLLMStatus( + current, + liteStatus, + spec.gatewayModelCapabilities, + spec.gatewayModelSlug, + spec.listedModelGatewayId, + ), ); const auth = yield* makeLiteLLMAuth({ instanceId: input.instanceId, @@ -687,16 +698,12 @@ export const withLiteLLMLauncher = ( orchestrationAdapter: { ...adapter, launchGeneration, - ...(spec.harness === "pi" - ? {} - : { - launchTags: (policy) => - withRequestTags(policy).pipe( - Effect.map( - ({ launchEnvironment }) => launchEnvironment?.[LITELLM_REQUEST_TAGS_ENV] ?? "", - ), - ), - }), + launchTags: (policy) => + withRequestTags(policy).pipe( + Effect.map( + ({ launchEnvironment }) => launchEnvironment?.[LITELLM_REQUEST_TAGS_ENV] ?? "", + ), + ), // New threads get their own session, so a new thread never inherits an old // key or model list. Threads already on a shared session keep sharing it. getCapabilities: () => adapter.getCapabilities().pipe(Effect.map(perThreadSessions)),