Skip to content

Commit e6b423e

Browse files
committed
🤖 feat(site/src/api): AI provider API client and query layer
Adds the frontend layer that talks to the existing /api/v2/ai/providers endpoints already shipped on main: - API client: getAIProviders, getAIProvider, createAIProvider, updateAIProvider, deleteAIProvider. - React Query wrappers in queries/aiProviders.ts with a shared key helper and matching cache invalidations. - Mock fixtures for OpenAI, Anthropic, and Bedrock providers in testHelpers/entities.ts for stories and unit tests. - viewAnyAIProvider registered in permissions.json so the existing permissions hook can read it. - viewAnyAIProvider added to canViewDeploymentSettings so admins who can only manage providers still see the deployment dropdown. No UI yet, the components and pages land in subsequent PRs.
1 parent faeb9d9 commit e6b423e

6 files changed

Lines changed: 189 additions & 15 deletions

File tree

site/.knip.jsonc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"ignore": [
1212
"**/*Generated.ts",
1313
"src/api/chatModelOptions.ts",
14+
// TODO(ai-settings): aiProviders.ts queries are staged in PR 2 of the
15+
// AI settings stack; they are consumed by the provider pages in PR 4.
16+
// Remove this exclusion once those pages land.
17+
"src/api/queries/aiProviders.ts",
1418
// TODO(devtools): debugPanelUtils.ts is staged in PR 7; its exports are
1519
// consumed by the Debug panel components in PRs 8 and 9. Remove this
1620
// exclusion once the panel components land.

site/permissions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
"object": { "resource_type": "aibridge_interception", "any_org": true },
104104
"action": "read"
105105
},
106+
"viewAnyAIProvider": {
107+
"object": { "resource_type": "ai_provider" },
108+
"action": "read"
109+
},
106110
"createOAuth2App": {
107111
"object": { "resource_type": "oauth2_app" },
108112
"action": "create"

site/src/api/api.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3076,6 +3076,47 @@ class ApiMethods {
30763076
const response = await this.axios.get<string[]>(url);
30773077
return response.data;
30783078
};
3079+
3080+
getAIProviders = async (): Promise<TypesGen.AIProvider[]> => {
3081+
const response = await this.axios.get<TypesGen.AIProvider[]>(
3082+
"/api/v2/ai/providers",
3083+
);
3084+
return response.data;
3085+
};
3086+
3087+
getAIProvider = async (idOrName: string): Promise<TypesGen.AIProvider> => {
3088+
const response = await this.axios.get<TypesGen.AIProvider>(
3089+
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
3090+
);
3091+
return response.data;
3092+
};
3093+
3094+
createAIProvider = async (
3095+
req: TypesGen.CreateAIProviderRequest,
3096+
): Promise<TypesGen.AIProvider> => {
3097+
const response = await this.axios.post<TypesGen.AIProvider>(
3098+
"/api/v2/ai/providers",
3099+
req,
3100+
);
3101+
return response.data;
3102+
};
3103+
3104+
updateAIProvider = async (
3105+
idOrName: string,
3106+
req: TypesGen.UpdateAIProviderRequest,
3107+
): Promise<TypesGen.AIProvider> => {
3108+
const response = await this.axios.patch<TypesGen.AIProvider>(
3109+
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
3110+
req,
3111+
);
3112+
return response.data;
3113+
};
3114+
3115+
deleteAIProvider = async (idOrName: string): Promise<void> => {
3116+
await this.axios.delete(
3117+
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
3118+
);
3119+
};
30793120
}
30803121

30813122
export type TaskFeedbackRating = "good" | "okay" | "bad";
@@ -3148,6 +3189,20 @@ class ExperimentalApiMethods {
31483189
};
31493190

31503191
// Chat API methods
3192+
getChatACL = async (chatId: string): Promise<TypesGen.ChatACL> => {
3193+
const response = await this.axios.get<TypesGen.ChatACL>(
3194+
`/api/experimental/chats/${chatId}/acl`,
3195+
);
3196+
return response.data;
3197+
};
3198+
3199+
updateChatACL = async (
3200+
chatId: string,
3201+
req: TypesGen.UpdateChatACL,
3202+
): Promise<void> => {
3203+
await this.axios.patch(`/api/experimental/chats/${chatId}/acl`, req);
3204+
};
3205+
31513206
getChats = async (req?: {
31523207
after_id?: string;
31533208
limit?: number;
@@ -3165,20 +3220,6 @@ class ExperimentalApiMethods {
31653220
);
31663221
return response.data;
31673222
};
3168-
getChatACL = async (chatId: string): Promise<TypesGen.ChatACL> => {
3169-
const response = await this.axios.get<TypesGen.ChatACL>(
3170-
`/api/experimental/chats/${chatId}/acl`,
3171-
);
3172-
return response.data;
3173-
};
3174-
3175-
updateChatACL = async (
3176-
chatId: string,
3177-
req: TypesGen.UpdateChatACL,
3178-
): Promise<void> => {
3179-
await this.axios.patch(`/api/experimental/chats/${chatId}/acl`, req);
3180-
};
3181-
31823223
getChatMessages = async (
31833224
chatId: string,
31843225
opts?: { before_id?: number; after_id?: number; limit?: number },
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { QueryClient } from "react-query";
2+
import { API } from "#/api/api";
3+
import type {
4+
AIProvider,
5+
CreateAIProviderRequest,
6+
UpdateAIProviderRequest,
7+
} from "#/api/typesGenerated";
8+
9+
const aiProvidersListKey = ["ai", "providers"] as const;
10+
11+
const aiProviderKeyFor = (idOrName: string) =>
12+
[...aiProvidersListKey, idOrName] as const;
13+
14+
export const aiProvidersList = () => ({
15+
queryKey: aiProvidersListKey,
16+
queryFn: (): Promise<AIProvider[]> => API.getAIProviders(),
17+
});
18+
19+
export const aiProvider = (idOrName: string) => ({
20+
queryKey: aiProviderKeyFor(idOrName),
21+
queryFn: (): Promise<AIProvider> => API.getAIProvider(idOrName),
22+
});
23+
24+
export const createAIProviderMutation = (queryClient: QueryClient) => ({
25+
mutationFn: (request: CreateAIProviderRequest): Promise<AIProvider> =>
26+
API.createAIProvider(request),
27+
onSuccess: async () => {
28+
await queryClient.invalidateQueries({ queryKey: aiProvidersListKey });
29+
},
30+
});
31+
32+
export const updateAIProviderMutation = (
33+
queryClient: QueryClient,
34+
idOrName: string,
35+
) => ({
36+
mutationFn: (request: UpdateAIProviderRequest): Promise<AIProvider> =>
37+
API.updateAIProvider(idOrName, request),
38+
onSuccess: async () => {
39+
await queryClient.invalidateQueries({ queryKey: aiProvidersListKey });
40+
await queryClient.invalidateQueries({
41+
queryKey: aiProviderKeyFor(idOrName),
42+
});
43+
},
44+
});
45+
46+
export const deleteAIProviderMutation = (
47+
queryClient: QueryClient,
48+
idOrName: string,
49+
) => ({
50+
mutationFn: () => API.deleteAIProvider(idOrName),
51+
onSuccess: async () => {
52+
await queryClient.invalidateQueries({ queryKey: aiProvidersListKey });
53+
queryClient.removeQueries({ queryKey: aiProviderKeyFor(idOrName) });
54+
},
55+
});

site/src/modules/permissions/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export const canViewDeploymentSettings = (
2525
permissions.viewAllUsers ||
2626
permissions.viewAnyGroup ||
2727
permissions.viewNotificationTemplate ||
28-
permissions.viewOrganizationIDPSyncSettings)
28+
permissions.viewOrganizationIDPSyncSettings ||
29+
permissions.viewAnyAIProvider)
2930
);
3031
};
3132

site/src/testHelpers/entities.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3298,6 +3298,7 @@ export const MockPermissions: Permissions = {
32983298
viewAnyIdpSyncSettings: true,
32993299
viewAnyMembers: true,
33003300
viewAnyAIBridgeInterception: true,
3301+
viewAnyAIProvider: true,
33013302
createOAuth2App: true,
33023303
editOAuth2App: true,
33033304
deleteOAuth2App: true,
@@ -3332,6 +3333,7 @@ export const MockNoPermissions: Permissions = {
33323333
viewAnyIdpSyncSettings: false,
33333334
viewAnyMembers: false,
33343335
viewAnyAIBridgeInterception: true,
3336+
viewAnyAIProvider: false,
33353337
createOAuth2App: false,
33363338
editOAuth2App: false,
33373339
deleteOAuth2App: false,
@@ -5515,3 +5517,70 @@ export const MockSession: TypesGen.AIBridgeSession = {
55155517
last_prompt: "But *can* I really fix it?",
55165518
last_active_at: "2026-03-09T10:28:15.03152Z",
55175519
};
5520+
5521+
/** @lintignore Consumed by component stories landing in the next PR of the AI settings stack. */
5522+
export const MockAIProviderOpenAI: TypesGen.AIProvider = {
5523+
id: "7a5d6b6a-5f02-4a9c-9c4e-2b3e2a3d2f01",
5524+
type: "openai",
5525+
name: "openai",
5526+
display_name: "OpenAI",
5527+
base_url: "https://api.openai.com",
5528+
enabled: false,
5529+
api_keys: [
5530+
{
5531+
id: "6d7c1f3a-1f0b-4a12-a1b5-0fb1f8e72e01",
5532+
masked: "sk-***\u2026***ABCD",
5533+
created_at: "2026-05-14T10:00:00Z",
5534+
},
5535+
],
5536+
settings: null as unknown as TypesGen.AIProviderSettings,
5537+
created_at: "2026-05-14T10:00:00Z",
5538+
updated_at: "2026-05-14T10:00:00Z",
5539+
};
5540+
5541+
/** @lintignore Consumed by component stories landing in the next PR of the AI settings stack. */
5542+
export const MockAIProviderAnthropic: TypesGen.AIProvider = {
5543+
id: "4f81f1ee-37c1-4a37-a9d5-7e0c1c8c0c11",
5544+
type: "anthropic",
5545+
name: "anthropic",
5546+
display_name: "Anthropic",
5547+
base_url: "https://api.anthropic.com",
5548+
enabled: false,
5549+
api_keys: [],
5550+
settings: null as unknown as TypesGen.AIProviderSettings,
5551+
created_at: "2026-05-14T10:00:00Z",
5552+
updated_at: "2026-05-14T10:00:00Z",
5553+
};
5554+
5555+
/**
5556+
* Bedrock providers come over the wire with `type: "anthropic"` and a
5557+
* `settings._type: "bedrock"` discriminator. `isBedrockProvider` and the
5558+
* backend (see `coderd/ai_providers.go`) enforce this convention.
5559+
*
5560+
* @lintignore Consumed by component stories landing in the next PR of the AI settings stack.
5561+
*/
5562+
export const MockAIProviderBedrock: TypesGen.AIProvider = {
5563+
id: "9c2e3b41-2e9f-4c97-9a4f-2e1a3d8f9f21",
5564+
type: "anthropic",
5565+
name: "bedrock",
5566+
display_name: "Bedrock",
5567+
base_url: "https://bedrock-runtime.us-east-2.amazonaws.com",
5568+
enabled: true,
5569+
api_keys: [],
5570+
settings: {
5571+
_type: "bedrock",
5572+
_version: 1,
5573+
region: "us-east-2",
5574+
model: "anthropic.claude-opus-4-7",
5575+
small_fast_model: "anthropic.claude-haiku-4-5",
5576+
} as unknown as TypesGen.AIProviderSettings,
5577+
created_at: "2026-05-14T10:00:00Z",
5578+
updated_at: "2026-05-14T10:00:00Z",
5579+
};
5580+
5581+
/** @lintignore Consumed by page stories landing in PR 4 of the AI settings stack. */
5582+
export const MockAIProviders: TypesGen.AIProvider[] = [
5583+
MockAIProviderOpenAI,
5584+
MockAIProviderAnthropic,
5585+
MockAIProviderBedrock,
5586+
];

0 commit comments

Comments
 (0)