mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support adding GitHub Copilot AI provider via UI (#25888)
Copilot is the only AI provider type that could not be added through the `/ai/settings` UI. The aibridge runtime and the env-var seeding path already supported it, but the runtime CRUD API rejected `type=copilot` and the UI omitted it entirely. The root cause is that Copilot's auth model (a per-request GitHub OAuth token, with no pre-shared key) does not fit the credential-centric add-provider flow that every other provider uses. ## Backend Allow `type=copilot` in `CreateAIProviderRequest.Validate()`, and reject `api_keys` for Copilot on both create (validation) and update (handler sentinel), mirroring the existing Bedrock guards. Copilot carries no stored credential. ## Frontend Add Copilot to the provider type picker (with the `github-copilot.svg` icon) and give the form a credential-free branch: name, display name, and a free-text endpoint defaulting to `https://api.business.githubcopilot.com`, with copy explaining that authentication happens via the user's GitHub token at request time. Copilot maps to the distinct `copilot` wire type rather than collapsing to `openai`, and the edit flow recovers it correctly. The endpoint stays required with a business-tier default; users on the individual or enterprise endpoints edit the field. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Generated
+3
-2
@@ -3240,8 +3240,9 @@ export interface ConvertLoginRequest {
|
||||
/**
|
||||
* CreateAIProviderRequest is the payload for creating a new AI
|
||||
* provider. Name and Type are required. APIKeys carries the plaintext
|
||||
* keys for OpenAI/Anthropic providers; Bedrock providers authenticate
|
||||
* via Settings and must omit APIKeys.
|
||||
* keys for OpenAI/Anthropic providers; Bedrock and Copilot providers
|
||||
* must omit APIKeys (Bedrock authenticates via Settings, Copilot via
|
||||
* request-time GitHub OAuth tokens).
|
||||
*/
|
||||
export interface CreateAIProviderRequest {
|
||||
readonly type: AIProviderType;
|
||||
|
||||
+23
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { within } from "storybook/test";
|
||||
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||
import { withToaster } from "#/testHelpers/storybook";
|
||||
import { addableProviders } from "../components/addableProviderTypes";
|
||||
@@ -26,16 +27,38 @@ export const AddAnthropic: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "anthropic")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add an Anthropic provider");
|
||||
},
|
||||
};
|
||||
|
||||
export const AddOpenAI: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "openai")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add an OpenAI provider");
|
||||
},
|
||||
};
|
||||
|
||||
export const AddBedrock: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "bedrock")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add an AWS Bedrock provider");
|
||||
},
|
||||
};
|
||||
|
||||
export const AddCopilot: Story = {
|
||||
args: {
|
||||
provider: addableProviders.find((p) => p.value === "copilot")!,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("Add a GitHub Copilot provider");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,9 @@ interface AddProviderPageViewProps {
|
||||
provider: AddableProvider;
|
||||
}
|
||||
|
||||
const indefiniteArticle = (word: string): string =>
|
||||
/^[aeiou]/i.test(word) ? "an" : "a";
|
||||
|
||||
const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
provider,
|
||||
}) => {
|
||||
@@ -38,7 +41,9 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
size="lg"
|
||||
src={getProviderIcon(provider.value)}
|
||||
/>
|
||||
<SettingsHeaderTitle>{`Add a ${provider.label} provider`}</SettingsHeaderTitle>
|
||||
<SettingsHeaderTitle>{`Add ${indefiniteArticle(
|
||||
provider.label,
|
||||
)} ${provider.label} provider`}</SettingsHeaderTitle>
|
||||
</div>
|
||||
<p className="text-sm text-content-secondary m-0">
|
||||
Configure connection details and credentials.
|
||||
|
||||
+16
@@ -5,6 +5,7 @@ import type { AIProvider } from "#/api/typesGenerated";
|
||||
import {
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
MockAIProviderCopilot,
|
||||
MockAIProviderOpenAI,
|
||||
} from "#/testHelpers/entities";
|
||||
import { withToaster } from "#/testHelpers/storybook";
|
||||
@@ -53,6 +54,21 @@ export const Bedrock: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
// Copilot has no stored credential, so the edit form renders no API key
|
||||
// field and keeps the immutable name disabled.
|
||||
export const Copilot: Story = {
|
||||
parameters: {
|
||||
reactRouter: routingFor(`/ai/settings/${MockAIProviderCopilot.name}`),
|
||||
...seed(MockAIProviderCopilot),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const name = await canvas.findByLabelText(/^name/i);
|
||||
expect(name).toBeDisabled();
|
||||
expect(canvas.queryByLabelText(/api key/i)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
// No seeded query: the page renders the loader while useQuery fetches.
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
|
||||
+8
-4
@@ -43,8 +43,12 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
});
|
||||
|
||||
const provider = providerQuery.data;
|
||||
const providerIsOpenAiAnthropic =
|
||||
provider !== undefined && !isBedrockProvider(provider);
|
||||
// Copilot has no stored credential, and Bedrock keeps its secrets in
|
||||
// settings, so only the remaining types surface the api_keys UI.
|
||||
const providerUsesApiKeys =
|
||||
provider !== undefined &&
|
||||
!isBedrockProvider(provider) &&
|
||||
provider.type !== "copilot";
|
||||
|
||||
const updateMutation = useMutation(
|
||||
updateAIProviderMutation(queryClient, providerId ?? ""),
|
||||
@@ -109,8 +113,8 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
}
|
||||
|
||||
const openAiAnthropicSavedApiKey =
|
||||
providerIsOpenAiAnthropic && provider.api_keys.length > 0;
|
||||
const openAiAnthropicMaskedApiKey = providerIsOpenAiAnthropic
|
||||
providerUsesApiKeys && provider.api_keys.length > 0;
|
||||
const openAiAnthropicMaskedApiKey = providerUsesApiKeys
|
||||
? provider.api_keys[0]?.masked
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -64,6 +64,38 @@ export const EditBedrockKeepCredentials: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AddCopilot: Story = {
|
||||
args: {
|
||||
// The real add flow passes only the type; the form fills name and
|
||||
// endpoint from the copilot defaults.
|
||||
initialValues: { type: "copilot" },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByLabelText(/endpoint/i);
|
||||
expect(canvas.queryByLabelText(/api key/i)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const EditCopilot: Story = {
|
||||
args: {
|
||||
editing: true,
|
||||
initialValues: {
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
displayName: "GitHub Copilot",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const name = await canvas.findByLabelText(/^name/i);
|
||||
expect(name).toBeDisabled();
|
||||
expect(canvas.queryByLabelText(/api key/i)).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const EditProvider: Story = {
|
||||
args: {
|
||||
editing: true,
|
||||
|
||||
@@ -81,6 +81,10 @@ const providerDefaults: Partial<
|
||||
name: "azure",
|
||||
baseUrl: "https://YOUR-RESOURCE.openai.azure.com/openai/v1",
|
||||
},
|
||||
copilot: {
|
||||
name: "copilot",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
},
|
||||
google: {
|
||||
name: "google",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
@@ -164,6 +168,20 @@ const makeBedrockSchema = (editing: boolean) =>
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
const makeCopilotSchema = (editing: boolean) =>
|
||||
Yup.object({
|
||||
type: Yup.string()
|
||||
.oneOf(["copilot"] as const)
|
||||
.required(),
|
||||
name: makeNameSchema(editing),
|
||||
displayName: makeDisplayNameSchema(editing),
|
||||
baseUrl: Yup.string()
|
||||
.url("Endpoint must be a valid URL")
|
||||
.matches(HTTP_SCHEME_REGEX, "Endpoint must use http or https.")
|
||||
.required("Endpoint is required"),
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
const getProviderFormSchema = (editing: boolean) =>
|
||||
Yup.lazy((value: { type?: AIProviderType } | undefined) => {
|
||||
switch (value?.type) {
|
||||
@@ -177,6 +195,8 @@ const getProviderFormSchema = (editing: boolean) =>
|
||||
return makeOpenAiAnthropicSchema(editing);
|
||||
case "bedrock":
|
||||
return makeBedrockSchema(editing);
|
||||
case "copilot":
|
||||
return makeCopilotSchema(editing);
|
||||
default:
|
||||
return Yup.object({
|
||||
type: Yup.string()
|
||||
@@ -185,6 +205,7 @@ const getProviderFormSchema = (editing: boolean) =>
|
||||
"anthropic",
|
||||
"bedrock",
|
||||
"azure",
|
||||
"copilot",
|
||||
"google",
|
||||
"openai-compat",
|
||||
"openrouter",
|
||||
@@ -319,18 +340,38 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
required
|
||||
field={getFieldHelpers("baseUrl")}
|
||||
label="Endpoint"
|
||||
description="The base URL where the provider's API is hosted."
|
||||
description={
|
||||
typeSelectValue === "copilot" ? (
|
||||
<>
|
||||
The base URL for your Copilot tier:{" "}
|
||||
<code>https://api.individual.githubcopilot.com</code>,{" "}
|
||||
<code>https://api.business.githubcopilot.com</code>, or{" "}
|
||||
<code>https://api.enterprise.githubcopilot.com</code>.
|
||||
</>
|
||||
) : (
|
||||
"The base URL where the provider's API is hosted."
|
||||
)
|
||||
}
|
||||
className="w-full"
|
||||
placeholder={baseUrlPlaceholder(form.values.type)}
|
||||
/>
|
||||
<CredentialField
|
||||
required
|
||||
label="API key"
|
||||
helpers={getFieldHelpers("apiKey")}
|
||||
onFocus={() => handleCredentialFocus("apiKey")}
|
||||
autoComplete="new-password"
|
||||
placeholder={apiKeyPlaceholder(form.values.type)}
|
||||
/>
|
||||
{typeSelectValue === "copilot" ? (
|
||||
<p className="text-sm text-content-secondary m-0">
|
||||
Copilot authenticates with each user's GitHub OAuth token at
|
||||
request time, so there is no API key to configure here. This
|
||||
requires a GitHub external authentication provider to be
|
||||
configured.
|
||||
</p>
|
||||
) : (
|
||||
<CredentialField
|
||||
required
|
||||
label="API key"
|
||||
helpers={getFieldHelpers("apiKey")}
|
||||
onFocus={() => handleCredentialFocus("apiKey")}
|
||||
autoComplete="new-password"
|
||||
placeholder={apiKeyPlaceholder(form.values.type)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -409,7 +450,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
disabled={isLoading || !form.dirty || !form.isValid}
|
||||
disabled={isLoading || !form.isValid || (editing && !form.dirty)}
|
||||
type="submit"
|
||||
>
|
||||
<Spinner loading={isLoading} />
|
||||
|
||||
@@ -27,6 +27,12 @@ export const Bedrock: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Copilot: Story = {
|
||||
args: {
|
||||
provider: "copilot",
|
||||
},
|
||||
};
|
||||
|
||||
export const Azure: Story = {
|
||||
args: {
|
||||
provider: "azure",
|
||||
|
||||
@@ -15,6 +15,8 @@ export const getProviderIcon = (provider: string): string | undefined => {
|
||||
return "/icon/aws.svg";
|
||||
case "azure":
|
||||
return "/icon/azure.svg";
|
||||
case "copilot":
|
||||
return "/icon/github-copilot.svg";
|
||||
case "google":
|
||||
return "/icon/google.svg";
|
||||
case "vercel":
|
||||
@@ -34,6 +36,8 @@ const getProviderName = (provider: string): string => {
|
||||
return "AWS Bedrock";
|
||||
case "azure":
|
||||
return "Azure OpenAI";
|
||||
case "copilot":
|
||||
return "GitHub Copilot";
|
||||
case "google":
|
||||
return "Google";
|
||||
case "openai-compat":
|
||||
|
||||
@@ -9,6 +9,7 @@ export const addableProviders: readonly AddableProvider[] = [
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "bedrock", label: "AWS Bedrock" },
|
||||
{ value: "azure", label: "Azure OpenAI" },
|
||||
{ value: "copilot", label: "GitHub Copilot" },
|
||||
{ value: "google", label: "Google" },
|
||||
{ value: "openai", label: "OpenAI" },
|
||||
{ value: "openai-compat", label: "OpenAI-compatible" },
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AIProvider } from "#/api/typesGenerated";
|
||||
import {
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
MockAIProviderCopilot,
|
||||
MockAIProviderOpenAI,
|
||||
} from "#/testHelpers/entities";
|
||||
import {
|
||||
@@ -45,6 +46,19 @@ const baseBedrockFormValues: ProviderFormValues = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const baseCopilotFormValues: ProviderFormValues = {
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
displayName: "GitHub Copilot",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
model: "",
|
||||
smallFastModel: "",
|
||||
accessKey: "",
|
||||
accessKeySecret: "",
|
||||
apiKey: "",
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Cast a plain object to AIProvider's discriminated `settings` shape;
|
||||
// the generated TS interface is empty and the wire form carries the
|
||||
// discriminator keys flattened in alongside the variant fields.
|
||||
@@ -173,6 +187,10 @@ describe("getProviderDisplayType", () => {
|
||||
expect(getProviderDisplayType(MockAIProviderOpenAI)).toBe("openai");
|
||||
});
|
||||
|
||||
it("returns copilot for a Copilot provider", () => {
|
||||
expect(getProviderDisplayType(MockAIProviderCopilot)).toBe("copilot");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["azure", "https://my-resource.openai.azure.com/openai/v1"],
|
||||
["azure", "https://YOUR-RESOURCE.openai.azure.com/openai/v1"],
|
||||
@@ -333,6 +351,31 @@ describe("providerFormValuesToCreate", () => {
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copilot", () => {
|
||||
it("maps to a distinct wire type with no api_keys", () => {
|
||||
const req = providerFormValuesToCreate(baseCopilotFormValues);
|
||||
expect(req.type).toBe("copilot");
|
||||
expect(req.base_url).toBe("https://api.business.githubcopilot.com");
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
|
||||
it("never sends api_keys even if the field carries a value", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseCopilotFormValues,
|
||||
apiKey: "should-be-ignored",
|
||||
});
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
});
|
||||
|
||||
it("omits display_name when blank so the server stores NULL", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseCopilotFormValues,
|
||||
displayName: "",
|
||||
});
|
||||
expect(req.display_name).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerFormValuesToUpdate", () => {
|
||||
@@ -460,6 +503,18 @@ describe("providerFormValuesToUpdate", () => {
|
||||
expect(s.access_key_secret).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copilot", () => {
|
||||
it("patches only the base fields and never sends api_keys", () => {
|
||||
const req = providerFormValuesToUpdate(
|
||||
{ ...baseCopilotFormValues, apiKey: "should-be-ignored" },
|
||||
MockAIProviderCopilot,
|
||||
);
|
||||
expect(req.api_keys).toBeUndefined();
|
||||
expect(req.settings).toBeUndefined();
|
||||
expect(req.base_url).toBe("https://api.business.githubcopilot.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("aiProviderToFormValues", () => {
|
||||
@@ -486,6 +541,14 @@ describe("aiProviderToFormValues", () => {
|
||||
expect(values.accessKeySecret).toBe("");
|
||||
});
|
||||
|
||||
it("seeds Copilot form values without a credential field", () => {
|
||||
const values = aiProviderToFormValues(MockAIProviderCopilot);
|
||||
expect(values.type).toBe("copilot");
|
||||
expect(values.name).toBe(MockAIProviderCopilot.name);
|
||||
expect(values.baseUrl).toBe(MockAIProviderCopilot.base_url);
|
||||
expect(values.apiKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to the slug when display_name is empty", () => {
|
||||
const provider: AIProvider = {
|
||||
...MockAIProviderOpenAI,
|
||||
|
||||
@@ -98,6 +98,9 @@ export const getProviderDisplayType = (
|
||||
if (provider.type === "anthropic") {
|
||||
return "anthropic";
|
||||
}
|
||||
if (provider.type === "copilot") {
|
||||
return "copilot";
|
||||
}
|
||||
const host = parseProviderHost(provider.base_url ?? "");
|
||||
const match = displayTypeHosts.find(([h]) => matchesHost(host, h));
|
||||
return match?.[1] ?? provider.type;
|
||||
@@ -125,12 +128,16 @@ const buildBedrockSettings = (
|
||||
export const providerFormValuesToCreate = (
|
||||
values: ProviderFormValues,
|
||||
): CreateAIProviderRequest => {
|
||||
const name = values.name.trim();
|
||||
const displayName = values.displayName.trim();
|
||||
const baseUrl = values.baseUrl.trim();
|
||||
const base: Omit<CreateAIProviderRequest, "type"> = {
|
||||
name: values.name.trim(),
|
||||
...(displayName ? { display_name: displayName } : {}),
|
||||
base_url: values.baseUrl.trim(),
|
||||
enabled: values.enabled,
|
||||
};
|
||||
|
||||
if (values.type === "bedrock") {
|
||||
const region = parseBedrockRegionFromBaseUrl(baseUrl);
|
||||
const region = parseBedrockRegionFromBaseUrl(base.base_url);
|
||||
const settings = buildBedrockSettings(
|
||||
region,
|
||||
values.model.trim(),
|
||||
@@ -140,17 +147,18 @@ export const providerFormValuesToCreate = (
|
||||
);
|
||||
return {
|
||||
type: "anthropic",
|
||||
name,
|
||||
...(displayName ? { display_name: displayName } : {}),
|
||||
base_url: baseUrl,
|
||||
enabled: values.enabled,
|
||||
...base,
|
||||
settings: settings as AIProviderSettings,
|
||||
};
|
||||
}
|
||||
|
||||
if (values.type === "copilot") {
|
||||
return { type: "copilot", ...base };
|
||||
}
|
||||
|
||||
const apiKey = sanitizeCredential(values.apiKey);
|
||||
// `""` is unreachable here (Yup blocks it, Bedrock branched out), but the
|
||||
// union still includes it; narrow so TS stays honest.
|
||||
// `""` is unreachable here (Yup blocks it, Bedrock and Copilot branched
|
||||
// out), but the union still includes it; narrow so TS stays honest.
|
||||
if (values.type === "") {
|
||||
throw new Error("provider type is required");
|
||||
}
|
||||
@@ -160,10 +168,7 @@ export const providerFormValuesToCreate = (
|
||||
values.type === "anthropic" ? "anthropic" : "openai";
|
||||
return {
|
||||
type: wireType,
|
||||
name,
|
||||
...(displayName ? { display_name: displayName } : {}),
|
||||
base_url: baseUrl,
|
||||
enabled: values.enabled,
|
||||
...base,
|
||||
...(apiKey ? { api_keys: [apiKey] } : {}),
|
||||
};
|
||||
};
|
||||
@@ -182,6 +187,10 @@ export const providerFormValuesToUpdate = (
|
||||
base_url: values.baseUrl.trim(),
|
||||
};
|
||||
|
||||
if (values.type === "copilot") {
|
||||
return base;
|
||||
}
|
||||
|
||||
if (values.type !== "bedrock") {
|
||||
// If the user didn't touch the input, the form still holds the seeded
|
||||
// mask and sanitizes to `""` (no rotation).
|
||||
@@ -240,8 +249,18 @@ export const aiProviderToFormValues = (
|
||||
};
|
||||
}
|
||||
|
||||
// Wire `type` is only `openai` or `anthropic`; the dropdown's richer
|
||||
// labels apply only on create.
|
||||
if (provider.type === "copilot") {
|
||||
return {
|
||||
type: "copilot",
|
||||
name: provider.name,
|
||||
displayName,
|
||||
baseUrl: provider.base_url,
|
||||
enabled: provider.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
// Wire `type` is otherwise only `openai` or `anthropic`; the dropdown's
|
||||
// richer labels apply only on create.
|
||||
return {
|
||||
type: provider.type === "anthropic" ? "anthropic" : "openai",
|
||||
name: provider.name,
|
||||
|
||||
@@ -5574,8 +5574,22 @@ export const MockAIProviderBedrock: TypesGen.AIProvider = {
|
||||
updated_at: "2026-05-14T10:00:00Z",
|
||||
};
|
||||
|
||||
export const MockAIProviderCopilot: TypesGen.AIProvider = {
|
||||
id: "b3f0d2c8-6a4e-4d11-8c2f-1e9a7c5b4d31",
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
display_name: "GitHub Copilot",
|
||||
base_url: "https://api.business.githubcopilot.com",
|
||||
enabled: true,
|
||||
api_keys: [],
|
||||
settings: null as unknown as TypesGen.AIProviderSettings,
|
||||
created_at: "2026-05-14T10:00:00Z",
|
||||
updated_at: "2026-05-14T10:00:00Z",
|
||||
};
|
||||
|
||||
export const MockAIProviders: TypesGen.AIProvider[] = [
|
||||
MockAIProviderOpenAI,
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
MockAIProviderCopilot,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user