Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/server/src/git/Layers/RoutingTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@ const makeRoutingTextGeneration = Effect.gen(function* () {
const codex = yield* CodexTextGen;
const claude = yield* ClaudeTextGen;

const route = (provider?: TextGenerationProvider): TextGenerationShape =>
provider === "claudeAgent" ? claude : codex;
const route = (provider?: TextGenerationProvider): TextGenerationShape => {
switch (provider) {
case "claudeAgent":
return claude;
case "codex":
case "glm":
case undefined:
return codex;
}
};

return {
generateCommitMessage: (input) =>
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/git/Services/TextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { ChatAttachment, ModelSelection } from "@t3tools/contracts";
import type { TextGenerationError } from "@t3tools/contracts";

/** Providers that support git text generation (commit messages, PR content, branch names). */
export type TextGenerationProvider = "codex" | "claudeAgent";
export type TextGenerationProvider = "codex" | "claudeAgent" | "glm";

export interface CommitMessageGenerationInput {
cwd: string;
Expand Down
140 changes: 140 additions & 0 deletions apps/server/src/provider/Layers/GlmAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type {
ApprovalRequestId,
ProviderApprovalDecision,
ProviderRuntimeEvent,
ProviderSendTurnInput,
ProviderSession,
ProviderSessionStartInput,
ProviderTurnStartResult,
ProviderUserInputAnswers,
ThreadId,
TurnId,
} from "@t3tools/contracts";
import { Effect, Layer, Queue, Stream } from "effect";

import type { ProviderAdapterError } from "../Errors.ts";
import { GlmAdapter, type GlmAdapterShape } from "../Services/GlmAdapter.ts";
import { CodexAdapter } from "../Services/CodexAdapter.ts";
import type {
ProviderAdapterCapabilities,
ProviderThreadSnapshot,
} from "../Services/ProviderAdapter.ts";
import type { EventNdjsonLogger } from "./EventNdjsonLogger.ts";

const PROVIDER = "glm" as const;

export interface GlmAdapterLiveOptions {
readonly nativeEventLogger?: EventNdjsonLogger;
}

function remapSessionProvider(session: ProviderSession): ProviderSession {
return { ...session, provider: PROVIDER };
}

export const GlmAdapterLive = Layer.effect(
GlmAdapter,
Effect.gen(function* () {
const codexAdapter = yield* CodexAdapter;
const glmEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
const glmThreadIds = new Set<ThreadId>();

const capabilities: ProviderAdapterCapabilities = {
sessionModelSwitch: "restart-session",
};

const startSession = (
input: ProviderSessionStartInput,
): Effect.Effect<ProviderSession, ProviderAdapterError> =>
Effect.gen(function* () {
glmThreadIds.add(input.threadId);
const session = yield* codexAdapter.startSession({
...input,
provider: "codex",
});
return remapSessionProvider(session);
});

const sendTurn = (
input: ProviderSendTurnInput,
): Effect.Effect<ProviderTurnStartResult, ProviderAdapterError> => codexAdapter.sendTurn(input);

const interruptTurn = (
threadId: ThreadId,
turnId?: TurnId,
): Effect.Effect<void, ProviderAdapterError> => codexAdapter.interruptTurn(threadId, turnId);

const respondToRequest = (
threadId: ThreadId,
requestId: ApprovalRequestId,
decision: ProviderApprovalDecision,
): Effect.Effect<void, ProviderAdapterError> =>
codexAdapter.respondToRequest(threadId, requestId, decision);

const respondToUserInput = (
threadId: ThreadId,
requestId: ApprovalRequestId,
answers: ProviderUserInputAnswers,
): Effect.Effect<void, ProviderAdapterError> =>
codexAdapter.respondToUserInput(threadId, requestId, answers);

const stopSession = (threadId: ThreadId): Effect.Effect<void, ProviderAdapterError> =>
Effect.gen(function* () {
yield* codexAdapter.stopSession(threadId);
glmThreadIds.delete(threadId);
});

const listSessions = (): Effect.Effect<ReadonlyArray<ProviderSession>> =>
codexAdapter
.listSessions()
.pipe(
Effect.map((sessions) =>
sessions.filter((s) => glmThreadIds.has(s.threadId)).map(remapSessionProvider),
),
);

const hasSession = (threadId: ThreadId): Effect.Effect<boolean> =>
glmThreadIds.has(threadId) ? codexAdapter.hasSession(threadId) : Effect.succeed(false);

const readThread = (
threadId: ThreadId,
): Effect.Effect<ProviderThreadSnapshot, ProviderAdapterError> =>
codexAdapter.readThread(threadId);

const rollbackThread = (
threadId: ThreadId,
numTurns: number,
): Effect.Effect<ProviderThreadSnapshot, ProviderAdapterError> =>
codexAdapter.rollbackThread(threadId, numTurns);

const stopAll = (): Effect.Effect<void, ProviderAdapterError> =>
Effect.gen(function* () {
for (const threadId of glmThreadIds) {
yield* codexAdapter.stopSession(threadId).pipe(Effect.ignore);
}
glmThreadIds.clear();
});

return {
provider: PROVIDER,
capabilities,
startSession,
sendTurn,
interruptTurn,
respondToRequest,
respondToUserInput,
stopSession,
listSessions,
hasSession,
readThread,
rollbackThread,
stopAll,
get streamEvents() {
return Stream.fromQueue(glmEventQueue);
},
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
} satisfies GlmAdapterShape;
}),
);

export function makeGlmAdapterLive(_options?: GlmAdapterLiveOptions) {
return GlmAdapterLive;
}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
127 changes: 127 additions & 0 deletions apps/server/src/provider/Layers/GlmProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { GlmSettings, ModelCapabilities, ServerProviderModel } from "@t3tools/contracts";
import { Effect, Equal, Layer, Stream } from "effect";

import {
buildServerProvider,
providerModelsFromSettings,
type ProviderProbeResult,
} from "../providerSnapshot.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import { GlmProvider } from "../Services/GlmProvider.ts";
import { ServerSettingsService } from "../../serverSettings.ts";

const PROVIDER = "glm" as const;

const DEFAULT_GLM_MODEL_CAPABILITIES: ModelCapabilities = {
reasoningEffortLevels: [],
supportsFastMode: false,
supportsThinkingToggle: false,
contextWindowOptions: [],
promptInjectedEffortLevels: [],
};

const BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
{
slug: "glm-5.1",
name: "GLM 5.1",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
{
slug: "glm-5",
name: "GLM 5",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
{
slug: "glm-5-turbo",
name: "GLM 5 Turbo",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
{
slug: "glm-4.7",
name: "GLM 4.7",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
{
slug: "glm-4.6",
name: "GLM 4.6",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
{
slug: "glm-4.5",
name: "GLM 4.5",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
{
slug: "glm-4.5-air",
name: "GLM 4.5 Air",
isCustom: false,
capabilities: DEFAULT_GLM_MODEL_CAPABILITIES,
},
];

function checkGlmProviderStatus(_glmSettings: GlmSettings): ProviderProbeResult {
const hasApiKey = Boolean(process.env.GLM_API_KEY);

if (!hasApiKey) {
return {
installed: true,
version: null,
status: "error",
auth: { status: "unauthenticated" },
message: "Set the GLM_API_KEY environment variable to authenticate.",
};
}

return {
installed: true,
version: null,
status: "ready",
auth: { status: "authenticated", type: "apiKey" },
};
}

export const GlmProviderLive = Layer.effect(
GlmProvider,
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;

const checkProvider = Effect.gen(function* () {
const settings = yield* serverSettings.getSettings;
const glmSettings = settings.providers.glm;
const probe = checkGlmProviderStatus(glmSettings);

const models = providerModelsFromSettings(
BUILT_IN_MODELS,
PROVIDER,
glmSettings.customModels,
DEFAULT_GLM_MODEL_CAPABILITIES,
);

return buildServerProvider({
provider: PROVIDER,
enabled: glmSettings.enabled,
checkedAt: new Date().toISOString(),
models,
probe,
});
});

return yield* makeManagedServerProvider<GlmSettings>({
getSettings: serverSettings.getSettings.pipe(
Effect.map((settings) => settings.providers.glm),
Effect.orDie,
),
streamSettings: serverSettings.streamChanges.pipe(
Stream.map((settings) => settings.providers.glm),
),
haveSettingsChanged: (previous, next) => !Equal.equals(previous, next),
checkProvider,
});
}),
);
23 changes: 22 additions & 1 deletion apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Effect, Layer, Stream } from "effect";

import { ClaudeAdapter, ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
import { GlmAdapter, GlmAdapterShape } from "../Services/GlmAdapter.ts";
import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
import { ProviderUnsupportedError } from "../Errors.ts";
Expand Down Expand Up @@ -45,13 +46,31 @@ const fakeClaudeAdapter: ClaudeAdapterShape = {
streamEvents: Stream.empty,
};

const fakeGlmAdapter: GlmAdapterShape = {
provider: "glm",
capabilities: { sessionModelSwitch: "restart-session" },
startSession: vi.fn(),
sendTurn: vi.fn(),
interruptTurn: vi.fn(),
respondToRequest: vi.fn(),
respondToUserInput: vi.fn(),
stopSession: vi.fn(),
listSessions: vi.fn(),
hasSession: vi.fn(),
readThread: vi.fn(),
rollbackThread: vi.fn(),
stopAll: vi.fn(),
streamEvents: Stream.empty,
};

const layer = it.layer(
Layer.mergeAll(
Layer.provide(
ProviderAdapterRegistryLive,
Layer.mergeAll(
Layer.succeed(CodexAdapter, fakeCodexAdapter),
Layer.succeed(ClaudeAdapter, fakeClaudeAdapter),
Layer.succeed(GlmAdapter, fakeGlmAdapter),
),
),
NodeServices.layer,
Expand All @@ -64,11 +83,13 @@ layer("ProviderAdapterRegistryLive", (it) => {
const registry = yield* ProviderAdapterRegistry;
const codex = yield* registry.getByProvider("codex");
const claude = yield* registry.getByProvider("claudeAgent");
const glm = yield* registry.getByProvider("glm");
assert.equal(codex, fakeCodexAdapter);
assert.equal(claude, fakeClaudeAdapter);
assert.equal(glm, fakeGlmAdapter);

const providers = yield* registry.listProviders();
assert.deepEqual(providers, ["codex", "claudeAgent"]);
assert.deepEqual(providers, ["codex", "claudeAgent", "glm"]);
}),
);

Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/Layers/ProviderAdapterRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "../Services/ProviderAdapterRegistry.ts";
import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts";
import { CodexAdapter } from "../Services/CodexAdapter.ts";
import { GlmAdapter } from "../Services/GlmAdapter.ts";

export interface ProviderAdapterRegistryLiveOptions {
readonly adapters?: ReadonlyArray<ProviderAdapterShape<ProviderAdapterError>>;
Expand All @@ -28,7 +29,7 @@ const makeProviderAdapterRegistry = Effect.fn("makeProviderAdapterRegistry")(fun
const adapters =
options?.adapters !== undefined
? options.adapters
: [yield* CodexAdapter, yield* ClaudeAdapter];
: [yield* CodexAdapter, yield* ClaudeAdapter, yield* GlmAdapter];
const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter]));

const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => {
Expand Down
Loading
Loading