diff --git a/apps/server/src/provider/Layers/PiProvider.test.ts b/apps/server/src/provider/Layers/PiProvider.test.ts index 4f7a6f6adf1f..c06de973e691 100644 --- a/apps/server/src/provider/Layers/PiProvider.test.ts +++ b/apps/server/src/provider/Layers/PiProvider.test.ts @@ -1,6 +1,8 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; +import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Queue from "effect/Queue"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; @@ -44,6 +46,56 @@ function piProbeSpawner(version: string) { }); } +function piDiscoverySpawner() { + return ChildProcessSpawner.make((command) => { + const args = ChildProcess.isStandardCommand(command) ? command.args : []; + if (args.includes("--version")) { + return Effect.succeed(processHandle({ stdout: "pi 1.0.0\n" })); + } + return Effect.gen(function* () { + const output = yield* Queue.unbounded(); + const exited = yield* Deferred.make(); + return ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(900_000_002), + exitCode: Deferred.await(exited), + isRunning: Effect.succeed(true), + kill: () => Deferred.succeed(exited, ChildProcessSpawner.ExitCode(0)).pipe(Effect.asVoid), + unref: Effect.succeed(Effect.void), + stdin: Sink.forEach((bytes: Uint8Array) => { + const request = JSON.parse(new TextDecoder().decode(bytes)); + const data = + request.type === "get_available_models" + ? { + models: [ + { provider: "openai-codex-work", id: "gpt-6-luna", name: "GPT-6 Luna" }, + { provider: "openai-codex-personal", id: "gpt-6-luna", name: "GPT-6 Luna" }, + ], + } + : request.type === "get_commands" + ? { commands: [] } + : {}; + return Queue.offer( + output, + encoder.encode( + JSON.stringify({ + type: "response", + id: request.id, + success: true, + data, + }) + "\n", + ), + ); + }), + stdout: Stream.fromQueue(output), + stderr: Stream.empty, + all: Stream.empty, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }); + }); + }); +} + const settings = { enabled: true, binaryPath: "pi", @@ -52,6 +104,27 @@ const settings = { } as const; describe("PiProvider", () => { + it.effect("preserves native provider labels for identically named Pi models", () => + Effect.gen(function* () { + const snapshot = yield* checkPiProviderStatus(settings).pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, piDiscoverySpawner()), + ); + assert.equal(snapshot.status, "ready"); + assert.deepEqual( + snapshot.models + .filter((model) => model.name === "GPT-6 Luna") + .map((model) => ({ + slug: model.slug, + subProvider: model.subProvider, + })), + [ + { slug: "openai-codex-work/gpt-6-luna", subProvider: "openai-codex-work" }, + { slug: "openai-codex-personal/gpt-6-luna", subProvider: "openai-codex-personal" }, + ], + ); + }).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("requires the first published Pi version with entries and settlement hooks", () => Effect.gen(function* () { const snapshot = yield* checkPiProviderStatus(settings).pipe( diff --git a/apps/server/src/provider/Layers/PiProvider.ts b/apps/server/src/provider/Layers/PiProvider.ts index edbace8019bd..6b11764f7f97 100644 --- a/apps/server/src/provider/Layers/PiProvider.ts +++ b/apps/server/src/provider/Layers/PiProvider.ts @@ -119,6 +119,7 @@ function parseDiscoveredModels( parsed.push({ slug, name: recordString(model, "name") ?? slug, + subProvider: provider, isCustom: false, capabilities: thinkingCapabilitiesForPiModel(model, defaultThinkingLevel), });