mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): add AI provider API client and query layer (#25580)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Linear: [DEVEX-355](https://linear.app/coder/issue/DEVEX-355) Second PR in a 5-PR stack splitting #25328. 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. The `aiProviders` query module and the per-provider mocks are temporarily added to the `knip` ignore list / annotated with `@lintignore`; the next PRs in the stack consume them and remove the exclusions. <details> <summary>Stack</summary> 1. #25579 jakehwll/DEVEX-355/01-primitives, primitives 2. **jakehwll/DEVEX-355/02-api, API client and query layer (this PR)** 3. jakehwll/DEVEX-355/03-components, provider form components 4. jakehwll/DEVEX-355/04-pages, pages and routes 5. jakehwll/DEVEX-355/05-section, section reshuffle Replaces #25328 once the stack lands. </details>
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
"ignore": [
|
||||
"**/*Generated.ts",
|
||||
"src/api/chatModelOptions.ts",
|
||||
// TODO(ai-settings): aiProviders.ts queries are staged in PR 2 of the
|
||||
// AI settings stack; they are consumed by the provider pages in PR 4.
|
||||
// Remove this exclusion once those pages land.
|
||||
"src/api/queries/aiProviders.ts",
|
||||
// TODO(devtools): debugPanelUtils.ts is staged in PR 7; its exports are
|
||||
// consumed by the Debug panel components in PRs 8 and 9. Remove this
|
||||
// exclusion once the panel components land.
|
||||
|
||||
@@ -103,6 +103,10 @@
|
||||
"object": { "resource_type": "aibridge_interception", "any_org": true },
|
||||
"action": "read"
|
||||
},
|
||||
"viewAnyAIProvider": {
|
||||
"object": { "resource_type": "ai_provider" },
|
||||
"action": "read"
|
||||
},
|
||||
"createOAuth2App": {
|
||||
"object": { "resource_type": "oauth2_app" },
|
||||
"action": "create"
|
||||
|
||||
+55
-14
@@ -3090,6 +3090,47 @@ class ApiMethods {
|
||||
const response = await this.axios.get<string[]>(url);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getAIProviders = async (): Promise<TypesGen.AIProvider[]> => {
|
||||
const response = await this.axios.get<TypesGen.AIProvider[]>(
|
||||
"/api/v2/ai/providers",
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
getAIProvider = async (idOrName: string): Promise<TypesGen.AIProvider> => {
|
||||
const response = await this.axios.get<TypesGen.AIProvider>(
|
||||
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
createAIProvider = async (
|
||||
req: TypesGen.CreateAIProviderRequest,
|
||||
): Promise<TypesGen.AIProvider> => {
|
||||
const response = await this.axios.post<TypesGen.AIProvider>(
|
||||
"/api/v2/ai/providers",
|
||||
req,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateAIProvider = async (
|
||||
idOrName: string,
|
||||
req: TypesGen.UpdateAIProviderRequest,
|
||||
): Promise<TypesGen.AIProvider> => {
|
||||
const response = await this.axios.patch<TypesGen.AIProvider>(
|
||||
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
|
||||
req,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
deleteAIProvider = async (idOrName: string): Promise<void> => {
|
||||
await this.axios.delete(
|
||||
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export type TaskFeedbackRating = "good" | "okay" | "bad";
|
||||
@@ -3162,6 +3203,20 @@ class ExperimentalApiMethods {
|
||||
};
|
||||
|
||||
// Chat API methods
|
||||
getChatACL = async (chatId: string): Promise<TypesGen.ChatACL> => {
|
||||
const response = await this.axios.get<TypesGen.ChatACL>(
|
||||
`/api/experimental/chats/${chatId}/acl`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateChatACL = async (
|
||||
chatId: string,
|
||||
req: TypesGen.UpdateChatACL,
|
||||
): Promise<void> => {
|
||||
await this.axios.patch(`/api/experimental/chats/${chatId}/acl`, req);
|
||||
};
|
||||
|
||||
getChats = async (req?: {
|
||||
after_id?: string;
|
||||
limit?: number;
|
||||
@@ -3179,20 +3234,6 @@ class ExperimentalApiMethods {
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
getChatACL = async (chatId: string): Promise<TypesGen.ChatACL> => {
|
||||
const response = await this.axios.get<TypesGen.ChatACL>(
|
||||
`/api/experimental/chats/${chatId}/acl`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
updateChatACL = async (
|
||||
chatId: string,
|
||||
req: TypesGen.UpdateChatACL,
|
||||
): Promise<void> => {
|
||||
await this.axios.patch(`/api/experimental/chats/${chatId}/acl`, req);
|
||||
};
|
||||
|
||||
getChatMessages = async (
|
||||
chatId: string,
|
||||
opts?: { before_id?: number; after_id?: number; limit?: number },
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { QueryClient } from "react-query";
|
||||
import { API } from "#/api/api";
|
||||
import type {
|
||||
AIProvider,
|
||||
CreateAIProviderRequest,
|
||||
UpdateAIProviderRequest,
|
||||
} from "#/api/typesGenerated";
|
||||
|
||||
const aiProvidersListKey = ["ai", "providers"] as const;
|
||||
|
||||
const aiProviderKeyFor = (idOrName: string) =>
|
||||
[...aiProvidersListKey, idOrName] as const;
|
||||
|
||||
export const aiProvidersList = () => ({
|
||||
queryKey: aiProvidersListKey,
|
||||
queryFn: (): Promise<AIProvider[]> => API.getAIProviders(),
|
||||
});
|
||||
|
||||
export const aiProvider = (idOrName: string) => ({
|
||||
queryKey: aiProviderKeyFor(idOrName),
|
||||
queryFn: (): Promise<AIProvider> => API.getAIProvider(idOrName),
|
||||
});
|
||||
|
||||
export const createAIProviderMutation = (queryClient: QueryClient) => ({
|
||||
mutationFn: (request: CreateAIProviderRequest): Promise<AIProvider> =>
|
||||
API.createAIProvider(request),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: aiProvidersListKey });
|
||||
},
|
||||
});
|
||||
|
||||
export const updateAIProviderMutation = (
|
||||
queryClient: QueryClient,
|
||||
idOrName: string,
|
||||
) => ({
|
||||
mutationFn: (request: UpdateAIProviderRequest): Promise<AIProvider> =>
|
||||
API.updateAIProvider(idOrName, request),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: aiProvidersListKey });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: aiProviderKeyFor(idOrName),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteAIProviderMutation = (
|
||||
queryClient: QueryClient,
|
||||
idOrName: string,
|
||||
) => ({
|
||||
mutationFn: () => API.deleteAIProvider(idOrName),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: aiProvidersListKey });
|
||||
queryClient.removeQueries({ queryKey: aiProviderKeyFor(idOrName) });
|
||||
},
|
||||
});
|
||||
@@ -25,7 +25,8 @@ export const canViewDeploymentSettings = (
|
||||
permissions.viewAllUsers ||
|
||||
permissions.viewAnyGroup ||
|
||||
permissions.viewNotificationTemplate ||
|
||||
permissions.viewOrganizationIDPSyncSettings)
|
||||
permissions.viewOrganizationIDPSyncSettings ||
|
||||
permissions.viewAnyAIProvider)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3298,6 +3298,7 @@ export const MockPermissions: Permissions = {
|
||||
viewAnyIdpSyncSettings: true,
|
||||
viewAnyMembers: true,
|
||||
viewAnyAIBridgeInterception: true,
|
||||
viewAnyAIProvider: true,
|
||||
createOAuth2App: true,
|
||||
editOAuth2App: true,
|
||||
deleteOAuth2App: true,
|
||||
@@ -3332,6 +3333,7 @@ export const MockNoPermissions: Permissions = {
|
||||
viewAnyIdpSyncSettings: false,
|
||||
viewAnyMembers: false,
|
||||
viewAnyAIBridgeInterception: true,
|
||||
viewAnyAIProvider: false,
|
||||
createOAuth2App: false,
|
||||
editOAuth2App: false,
|
||||
deleteOAuth2App: false,
|
||||
@@ -5515,3 +5517,70 @@ export const MockSession: TypesGen.AIBridgeSession = {
|
||||
last_prompt: "But *can* I really fix it?",
|
||||
last_active_at: "2026-03-09T10:28:15.03152Z",
|
||||
};
|
||||
|
||||
/** @lintignore Consumed by component stories landing in the next PR of the AI settings stack. */
|
||||
export const MockAIProviderOpenAI: TypesGen.AIProvider = {
|
||||
id: "7a5d6b6a-5f02-4a9c-9c4e-2b3e2a3d2f01",
|
||||
type: "openai",
|
||||
name: "openai",
|
||||
display_name: "OpenAI",
|
||||
base_url: "https://api.openai.com",
|
||||
enabled: false,
|
||||
api_keys: [
|
||||
{
|
||||
id: "6d7c1f3a-1f0b-4a12-a1b5-0fb1f8e72e01",
|
||||
masked: "sk-***\u2026***ABCD",
|
||||
created_at: "2026-05-14T10:00:00Z",
|
||||
},
|
||||
],
|
||||
settings: null as unknown as TypesGen.AIProviderSettings,
|
||||
created_at: "2026-05-14T10:00:00Z",
|
||||
updated_at: "2026-05-14T10:00:00Z",
|
||||
};
|
||||
|
||||
/** @lintignore Consumed by component stories landing in the next PR of the AI settings stack. */
|
||||
export const MockAIProviderAnthropic: TypesGen.AIProvider = {
|
||||
id: "4f81f1ee-37c1-4a37-a9d5-7e0c1c8c0c11",
|
||||
type: "anthropic",
|
||||
name: "anthropic",
|
||||
display_name: "Anthropic",
|
||||
base_url: "https://api.anthropic.com",
|
||||
enabled: false,
|
||||
api_keys: [],
|
||||
settings: null as unknown as TypesGen.AIProviderSettings,
|
||||
created_at: "2026-05-14T10:00:00Z",
|
||||
updated_at: "2026-05-14T10:00:00Z",
|
||||
};
|
||||
|
||||
/**
|
||||
* Bedrock providers come over the wire with `type: "anthropic"` and a
|
||||
* `settings._type: "bedrock"` discriminator. `isBedrockProvider` and the
|
||||
* backend (see `coderd/ai_providers.go`) enforce this convention.
|
||||
*
|
||||
* @lintignore Consumed by component stories landing in the next PR of the AI settings stack.
|
||||
*/
|
||||
export const MockAIProviderBedrock: TypesGen.AIProvider = {
|
||||
id: "9c2e3b41-2e9f-4c97-9a4f-2e1a3d8f9f21",
|
||||
type: "anthropic",
|
||||
name: "bedrock",
|
||||
display_name: "Bedrock",
|
||||
base_url: "https://bedrock-runtime.us-east-2.amazonaws.com",
|
||||
enabled: true,
|
||||
api_keys: [],
|
||||
settings: {
|
||||
_type: "bedrock",
|
||||
_version: 1,
|
||||
region: "us-east-2",
|
||||
model: "anthropic.claude-opus-4-7",
|
||||
small_fast_model: "anthropic.claude-haiku-4-5",
|
||||
} as unknown as TypesGen.AIProviderSettings,
|
||||
created_at: "2026-05-14T10:00:00Z",
|
||||
updated_at: "2026-05-14T10:00:00Z",
|
||||
};
|
||||
|
||||
/** @lintignore Consumed by page stories landing in PR 4 of the AI settings stack. */
|
||||
export const MockAIProviders: TypesGen.AIProvider[] = [
|
||||
MockAIProviderOpenAI,
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user