Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
34 changes: 34 additions & 0 deletions apps/app/scripts/workspace-endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test";
import { resolveWorkspaceEndpoint } from "../src/app/lib/workspace-endpoint";

describe("resolveWorkspaceEndpoint", () => {
test("does not use remote host token as bearer authorization", () => {
const endpoint = resolveWorkspaceEndpoint({
id: "rem_ws_123",
workspaceType: "remote",
baseUrl: "https://worker.example.test",
openworkHostUrl: "https://worker.example.test",
openworkToken: null,
openworkClientToken: null,
openworkHostToken: "host-token-must-not-be-bearer",
openworkWorkspaceId: "ws_123",
} as never, { baseUrl: "http://127.0.0.1:8791", token: "local-token" });

expect(endpoint?.token).toBe("");
});

test("uses remote client token before local server token", () => {
const endpoint = resolveWorkspaceEndpoint({
id: "rem_ws_123",
workspaceType: "remote",
baseUrl: "https://worker.example.test",
openworkHostUrl: "https://worker.example.test",
openworkToken: null,
openworkClientToken: "remote-client-token",
openworkHostToken: "host-token",
openworkWorkspaceId: "ws_123",
} as never, { baseUrl: "http://127.0.0.1:8791", token: "local-token" });

expect(endpoint?.token).toBe("remote-client-token");
});
});
63 changes: 63 additions & 0 deletions apps/app/src/app/cloud/managed-provider-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { CloudImportedProvider } from "./import-state";
import type { ModelOption, ProviderListItem } from "../types";

export function buildCloudManagedModelIdsByProvider(
importedCloudProviders: Record<string, CloudImportedProvider> | null | undefined,
): Map<string, Set<string>> {
const next = new Map<string, Set<string>>();
for (const imported of Object.values(importedCloudProviders ?? {})) {
const providerId = imported.providerId.trim();
if (!providerId) continue;
const modelIds = imported.modelIds.map((id) => id.trim()).filter(Boolean);
if (!modelIds.length) continue;
const merged = next.get(providerId) ?? new Set<string>();
for (const modelId of modelIds) merged.add(modelId);
next.set(providerId, merged);
}
return next;
}

export function isCloudManagedModelAllowed(
cloudManagedModelIdsByProvider: Map<string, Set<string>>,
providerId: string,
modelId: string,
) {
const allowedModelIds = cloudManagedModelIdsByProvider.get(providerId);
return !allowedModelIds || allowedModelIds.has(modelId);
}

export function hasCloudManagedModelAllowlist(
cloudManagedModelIdsByProvider: Map<string, Set<string>>,
providerId: string,
) {
return cloudManagedModelIdsByProvider.has(providerId);
}

export function buildCloudManagedModelOptions(input: {
providers: ProviderListItem[];
cloudManagedModelIdsByProvider: Map<string, Set<string>>;
isRecommendedProvider?: (providerId: string) => boolean;
}): ModelOption[] {
const options: ModelOption[] = [];
for (const provider of input.providers) {
const isCloudManaged = hasCloudManagedModelAllowlist(input.cloudManagedModelIdsByProvider, provider.id);
for (const [modelId, model] of Object.entries(provider.models)) {
if (!isCloudManagedModelAllowed(input.cloudManagedModelIdsByProvider, provider.id, modelId)) continue;
options.push({
providerID: provider.id,
modelID: modelId,
title: model.name || modelId,
description: provider.name,
behaviorTitle: "Reasoning",
behaviorLabel: "Default",
behaviorDescription: "",
behaviorValue: null,
isFree: false,
isConnected: true,
isRecommended: input.isRecommendedProvider?.(provider.id),
source: isCloudManaged || /^lpr_/i.test(provider.id) ? "cloud" : undefined,
});
}
}
return options;
}
60 changes: 59 additions & 1 deletion apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,29 @@ export type DenOrgLlmProviderModel = {
export type DenOrgLlmProvider = {
id: string;
source: "models_dev" | "custom" | "openwork";
credentialKind: "api_key" | "opencode_oauth";
providerId: string;
name: string;
providerConfig: Record<string, unknown>;
hasApiKey: boolean;
hasOpencodeAuth: boolean;
hasCredential: boolean;
models: DenOrgLlmProviderModel[];
createdAt: string | null;
updatedAt: string | null;
};

export type DenOrgLlmProviderConnection = DenOrgLlmProvider & {
apiKey: string | null;
opencodeAuth: string | null;
};

export type DenManagedProviderSyncResult = {
status: "applied" | "failed";
providerCount: number;
revision: string;
providerIds?: string[];
reason?: string;
};

export type DenPluginConfigObjectType = "skill" | "agent" | "command" | "tool" | "mcp" | "hook" | "context" | "custom";
Expand Down Expand Up @@ -1028,10 +1040,13 @@ function parseDenOrgLlmProvider(value: unknown): DenOrgLlmProvider | null {
return {
id: value.id,
source: value.source,
credentialKind: value.credentialKind === "opencode_oauth" ? "opencode_oauth" : "api_key",
providerId: value.providerId,
name: value.name,
providerConfig: parseJsonRecord(value.providerConfig),
hasApiKey: value.hasApiKey === true,
hasOpencodeAuth: value.hasOpencodeAuth === true,
hasCredential: value.hasCredential === true || value.hasApiKey === true || value.hasOpencodeAuth === true,
models: Array.isArray(value.models)
? value.models.flatMap((model) => {
const parsed = parseDenOrgLlmProviderModel(model);
Expand Down Expand Up @@ -1067,6 +1082,28 @@ function getDenOrgLlmProviderConnection(payload: unknown): DenOrgLlmProviderConn
return {
...provider,
apiKey: typeof payload.llmProvider.apiKey === "string" ? payload.llmProvider.apiKey : null,
opencodeAuth: typeof payload.llmProvider.opencodeAuth === "string" ? payload.llmProvider.opencodeAuth : null,
};
}

function getDenManagedProviderSyncResult(payload: unknown): DenManagedProviderSyncResult | null {
if (!isRecord(payload)) return null;
if (payload.status !== "applied" && payload.status !== "failed") return null;
if (typeof payload.providerCount !== "number" || !Number.isInteger(payload.providerCount) || payload.providerCount < 0) return null;
if (typeof payload.revision !== "string") return null;
const rawProviderIds = Array.isArray(payload.providerIds)
? payload.providerIds
: Array.isArray(payload.appliedProviderIds)
? payload.appliedProviderIds
: undefined;
const providerIds = rawProviderIds ? readStringArray(rawProviderIds) : undefined;
if (rawProviderIds && providerIds?.length !== payload.providerCount) return null;
return {
status: payload.status,
providerCount: payload.providerCount,
revision: payload.revision,
...(providerIds ? { providerIds } : {}),
...(typeof payload.reason === "string" ? { reason: payload.reason } : {}),
};
}

Expand Down Expand Up @@ -1880,7 +1917,7 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
},

async listOrgLlmProviders(orgId: string): Promise<DenOrgLlmProvider[]> {
const payload = await requestJson<unknown>(baseUrls, "/v1/llm-providers", {
const payload = await requestJson<unknown>(baseUrls, "/v1/llm-providers?scope=usable", {
method: "GET",
token,
organizationId: orgId,
Expand All @@ -1905,6 +1942,27 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
return provider;
},

async syncWorkerManagedProviders(orgId: string, workerId: string): Promise<DenManagedProviderSyncResult> {
const payload = await requestJson<unknown>(
baseUrls,
`/v1/workers/${encodeURIComponent(workerId)}/managed-providers/sync`,
{
method: "POST",
token,
organizationId: orgId,
body: {},
},
);
const result = getDenManagedProviderSyncResult(payload);
if (!result) {
throw new DenApiError(500, "invalid_managed_provider_sync_payload", "Managed provider sync response was invalid.");
}
if (result.status !== "applied") {
throw new DenApiError(502, "managed_provider_sync_failed", result.reason ?? "Managed provider sync failed.");
}
return result;
},

async listOrgMarketplaces(orgId: string): Promise<DenOrgMarketplace[]> {
const payload = await requestJson<unknown>(
baseUrls,
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/app/lib/desktop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export type WorkspaceInfo = {
openworkHostToken?: string | null;
openworkWorkspaceId?: string | null;
openworkWorkspaceName?: string | null;
openworkDenOrgId?: string | null;
openworkDenWorkerId?: string | null;
sandboxBackend?: "docker" | "microsandbox" | null;
sandboxRunId?: string | null;
sandboxContainerName?: string | null;
Expand Down
2 changes: 0 additions & 2 deletions apps/app/src/app/lib/workspace-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ type WorkspaceEndpointInput = Pick<
| "openworkHostUrl"
| "openworkToken"
| "openworkClientToken"
| "openworkHostToken"
| "openworkWorkspaceId"
> | null | undefined;

Expand Down Expand Up @@ -95,7 +94,6 @@ function pickRemoteToken(workspace: WorkspaceEndpointInput): string {
return (
workspace.openworkToken ??
workspace.openworkClientToken ??
workspace.openworkHostToken ??
""
).trim();
}
Expand Down
24 changes: 7 additions & 17 deletions apps/app/src/components/model-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { isDesktopProviderBlocked } from "@/app/cloud/desktop-app-restrictions";
import { openModelPickerEvent } from "@/react-app/shell/new-providers-toast";
import { newProvidersEvent } from "@/app/lib/provider-events";
import { buildCloudManagedModelOptions } from "@/app/cloud/managed-provider-models";

function getProviderDisplayName(providerId: string) {
return providerId
Expand All @@ -55,7 +56,7 @@ function getProviderDisplayName(providerId: string) {
}

function useModelOptions(open: boolean) {
const { client, opencodeBaseUrl, selectedWorkspaceRoot } = useWorkspace();
const { client, opencodeBaseUrl, selectedWorkspaceRoot, cloudManagedModelIdsByProvider } = useWorkspace();
const checkDesktopRestriction = useCheckDesktopRestriction();

const { data, refetch } = useProviderListQuery({
Expand Down Expand Up @@ -89,21 +90,10 @@ function useModelOptions(open: boolean) {
restriction: "allowCustomProviders",
});

const options = getConnectedProviderItems(data)
.flatMap((provider) =>
Object.entries(provider.models).map(([id, model]) => ({
providerID: provider.id,
modelID: id,
title: model.name,
description: provider.name,
behaviorTitle: "Reasoning",
behaviorLabel: "Default",
behaviorDescription: "",
behaviorValue: null,
isFree: false,
isConnected: true,
})),
);
const options = buildCloudManagedModelOptions({
providers: getConnectedProviderItems(data),
cloudManagedModelIdsByProvider,
});

return options.filter((option) => {
if (
Expand All @@ -121,7 +111,7 @@ function useModelOptions(open: boolean) {

return true;
});
}, [checkDesktopRestriction, data]);
}, [checkDesktopRestriction, cloudManagedModelIdsByProvider, data]);
}

type ModelSelectModelItem = {
Expand Down
11 changes: 9 additions & 2 deletions apps/app/src/react-app/domains/cloud/org-onboarding-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,9 +510,16 @@ interface ProviderCardProps {
} | null) => void;
}

function getCloudManagedProviderId(
provider: Pick<DenOrgLlmProvider, "id" | "providerId" | "source" | "credentialKind">,
) {
if (provider.source === "openwork") return "openwork";
if (provider.credentialKind === "opencode_oauth") return provider.providerId.trim();
return provider.id.trim();
}

function ProviderCard({ provider, selectedDefault, onSelectDefault }: ProviderCardProps) {
// The local provider ID matches the cloud provider's org-level ID
const localProviderId = provider.id.trim();
const localProviderId = getCloudManagedProviderId(provider);
const firstModel = provider.models[0] ?? null;
const isSelected = selectedDefault?.providerId === localProviderId;

Expand Down
Loading