mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-09 00:24:34 +00:00
fix(copilot): surface upstream model limits
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
getAvailableModels,
|
getAvailableModels,
|
||||||
isCopilotApiInstalled,
|
isCopilotApiInstalled,
|
||||||
} from '../copilot';
|
} from '../copilot';
|
||||||
|
import type { CopilotModel } from '../copilot';
|
||||||
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../config/unified-config-loader';
|
||||||
import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
|
import { DEFAULT_COPILOT_CONFIG } from '../config/unified-config-types';
|
||||||
import { ok, fail, info, color } from '../utils/ui';
|
import { ok, fail, info, color } from '../utils/ui';
|
||||||
@@ -195,14 +196,56 @@ async function handleModels(): Promise<number> {
|
|||||||
const defaultMark = model.isDefault ? ' (default)' : '';
|
const defaultMark = model.isDefault ? ' (default)' : '';
|
||||||
console.log(` ${model.id}${current}${defaultMark}`);
|
console.log(` ${model.id}${current}${defaultMark}`);
|
||||||
console.log(` Provider: ${model.provider}`);
|
console.log(` Provider: ${model.provider}`);
|
||||||
|
const limits = formatModelLimits(model);
|
||||||
|
if (limits) {
|
||||||
|
console.log(` Limits: ${limits}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
if (models.some((model) => formatModelLimits(model))) {
|
||||||
|
console.log('Live limits above come from GitHub Copilot model metadata.');
|
||||||
|
} else {
|
||||||
|
console.log('Live Copilot limits were unavailable. Start the daemon and rerun this command.');
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
'CCS can switch Copilot models, but it cannot raise GitHub Copilot prompt/context caps.'
|
||||||
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('To change model: ccs config (Copilot section)');
|
console.log('To change model: ccs config (Copilot section)');
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCompactTokens(value: number): string {
|
||||||
|
if (value >= 1_000_000) {
|
||||||
|
const millions = value / 1_000_000;
|
||||||
|
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1_000) {
|
||||||
|
const thousands = value / 1_000;
|
||||||
|
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatModelLimits(model: CopilotModel): string | null {
|
||||||
|
if (!model.limits) return null;
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (model.limits.maxPromptTokens) {
|
||||||
|
parts.push(`prompt ${formatCompactTokens(model.limits.maxPromptTokens)}`);
|
||||||
|
}
|
||||||
|
if (model.limits.maxContextWindowTokens) {
|
||||||
|
parts.push(`context ${formatCompactTokens(model.limits.maxContextWindowTokens)}`);
|
||||||
|
}
|
||||||
|
if (model.limits.maxOutputTokens) {
|
||||||
|
parts.push(`output ${formatCompactTokens(model.limits.maxOutputTokens)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' | ') : null;
|
||||||
|
}
|
||||||
|
|
||||||
function formatQuotaLine(
|
function formatQuotaLine(
|
||||||
label: string,
|
label: string,
|
||||||
snapshot: {
|
snapshot: {
|
||||||
|
|||||||
@@ -9,6 +9,27 @@
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { CopilotModel } from './types';
|
import { CopilotModel } from './types';
|
||||||
|
|
||||||
|
const DEFAULT_COPILOT_MODEL_ID = 'gpt-4.1';
|
||||||
|
const MAX_MODELS_BODY_SIZE = 1024 * 1024; // 1MB
|
||||||
|
|
||||||
|
interface CopilotDaemonModelLimits {
|
||||||
|
max_context_window_tokens?: unknown;
|
||||||
|
max_output_tokens?: unknown;
|
||||||
|
max_prompt_tokens?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotDaemonModel {
|
||||||
|
id?: unknown;
|
||||||
|
name?: unknown;
|
||||||
|
capabilities?: {
|
||||||
|
limits?: CopilotDaemonModelLimits;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CopilotModelsResponse {
|
||||||
|
data?: CopilotDaemonModel[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default models available through copilot-api.
|
* Default models available through copilot-api.
|
||||||
* Used as fallback when API is not reachable.
|
* Used as fallback when API is not reachable.
|
||||||
@@ -154,6 +175,13 @@ export const DEFAULT_COPILOT_MODELS: CopilotModel[] = [
|
|||||||
*/
|
*/
|
||||||
export async function fetchModelsFromDaemon(port: number): Promise<CopilotModel[]> {
|
export async function fetchModelsFromDaemon(port: number): Promise<CopilotModel[]> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
const safeResolve = (models: CopilotModel[]) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
resolve(models);
|
||||||
|
};
|
||||||
|
|
||||||
const req = http.request(
|
const req = http.request(
|
||||||
{
|
{
|
||||||
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
// Use 127.0.0.1 instead of localhost for more reliable local connections
|
||||||
@@ -168,36 +196,37 @@ export async function fetchModelsFromDaemon(port: number): Promise<CopilotModel[
|
|||||||
|
|
||||||
res.on('data', (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
data += chunk;
|
data += chunk;
|
||||||
|
if (data.length > MAX_MODELS_BODY_SIZE) {
|
||||||
|
req.destroy();
|
||||||
|
safeResolve(DEFAULT_COPILOT_MODELS);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const response = JSON.parse(data) as { data?: Array<{ id: string }> };
|
const response = JSON.parse(data) as CopilotModelsResponse;
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (Array.isArray(response.data)) {
|
||||||
const models: CopilotModel[] = response.data.map((m) => ({
|
const models = response.data
|
||||||
id: m.id,
|
.map(mapDaemonModel)
|
||||||
name: formatModelName(m.id),
|
.filter((model): model is CopilotModel => model !== null);
|
||||||
provider: detectProvider(m.id),
|
safeResolve(models.length > 0 ? models : DEFAULT_COPILOT_MODELS);
|
||||||
isDefault: m.id === 'gpt-4.1', // Free tier default
|
|
||||||
}));
|
|
||||||
resolve(models.length > 0 ? models : DEFAULT_COPILOT_MODELS);
|
|
||||||
} else {
|
} else {
|
||||||
resolve(DEFAULT_COPILOT_MODELS);
|
safeResolve(DEFAULT_COPILOT_MODELS);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
resolve(DEFAULT_COPILOT_MODELS);
|
safeResolve(DEFAULT_COPILOT_MODELS);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
req.on('error', () => {
|
req.on('error', () => {
|
||||||
resolve(DEFAULT_COPILOT_MODELS);
|
safeResolve(DEFAULT_COPILOT_MODELS);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('timeout', () => {
|
req.on('timeout', () => {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
resolve(DEFAULT_COPILOT_MODELS);
|
safeResolve(DEFAULT_COPILOT_MODELS);
|
||||||
});
|
});
|
||||||
|
|
||||||
req.end();
|
req.end();
|
||||||
@@ -216,7 +245,7 @@ export async function getAvailableModels(port: number): Promise<CopilotModel[]>
|
|||||||
* Uses gpt-4.1 as it's available on free tier.
|
* Uses gpt-4.1 as it's available on free tier.
|
||||||
*/
|
*/
|
||||||
export function getDefaultModel(): string {
|
export function getDefaultModel(): string {
|
||||||
return 'gpt-4.1';
|
return DEFAULT_COPILOT_MODEL_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -246,3 +275,40 @@ function formatModelName(modelId: string): string {
|
|||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLimitValue(value: unknown): number | undefined {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLimits(limits?: CopilotDaemonModelLimits): CopilotModel['limits'] | undefined {
|
||||||
|
if (!limits) return undefined;
|
||||||
|
|
||||||
|
const normalized = {
|
||||||
|
maxContextWindowTokens: normalizeLimitValue(limits.max_context_window_tokens),
|
||||||
|
maxOutputTokens: normalizeLimitValue(limits.max_output_tokens),
|
||||||
|
maxPromptTokens: normalizeLimitValue(limits.max_prompt_tokens),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.values(normalized).some((value) => value !== undefined) ? normalized : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDaemonModel(entry: CopilotDaemonModel): CopilotModel | null {
|
||||||
|
if (typeof entry.id !== 'string') return null;
|
||||||
|
const id = entry.id.trim();
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const defaultMeta = DEFAULT_COPILOT_MODELS.find((model) => model.id === id);
|
||||||
|
const limits = extractLimits(entry.capabilities?.limits);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultMeta,
|
||||||
|
id,
|
||||||
|
name:
|
||||||
|
typeof entry.name === 'string' && entry.name.trim().length > 0
|
||||||
|
? entry.name.trim()
|
||||||
|
: (defaultMeta?.name ?? formatModelName(id)),
|
||||||
|
provider: defaultMeta?.provider ?? detectProvider(id),
|
||||||
|
isDefault: id === DEFAULT_COPILOT_MODEL_ID,
|
||||||
|
...(limits ? { limits } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,6 +41,15 @@ export interface CopilotStatus {
|
|||||||
*/
|
*/
|
||||||
export type CopilotPlanTier = 'free' | 'pro' | 'pro+' | 'business' | 'enterprise';
|
export type CopilotPlanTier = 'free' | 'pro' | 'pro+' | 'business' | 'enterprise';
|
||||||
|
|
||||||
|
export interface CopilotModelLimits {
|
||||||
|
/** Maximum total context window exposed by GitHub Copilot */
|
||||||
|
maxContextWindowTokens?: number;
|
||||||
|
/** Maximum output/completion tokens exposed by GitHub Copilot */
|
||||||
|
maxOutputTokens?: number;
|
||||||
|
/** Maximum prompt/input tokens exposed by GitHub Copilot */
|
||||||
|
maxPromptTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copilot model information.
|
* Copilot model information.
|
||||||
*/
|
*/
|
||||||
@@ -57,6 +66,8 @@ export interface CopilotModel {
|
|||||||
multiplier?: number;
|
multiplier?: number;
|
||||||
/** Whether this model is in preview */
|
/** Whether this model is in preview */
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
|
/** Live model limits returned by GitHub Copilot metadata, when available */
|
||||||
|
limits?: CopilotModelLimits;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import * as http from 'http';
|
||||||
|
import { DEFAULT_COPILOT_MODELS, fetchModelsFromDaemon } from '../../../src/copilot/copilot-models';
|
||||||
|
|
||||||
|
describe('fetchModelsFromDaemon', () => {
|
||||||
|
it('falls back to defaults when daemon is unreachable', async () => {
|
||||||
|
const models = await fetchModelsFromDaemon(9999);
|
||||||
|
expect(models).toEqual(DEFAULT_COPILOT_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to defaults when daemon returns invalid JSON', async () => {
|
||||||
|
const server = http.createServer((_req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end('{not-valid-json');
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
throw new Error('Unable to resolve test server port');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = await fetchModelsFromDaemon(address.port);
|
||||||
|
expect(models).toEqual(DEFAULT_COPILOT_MODELS);
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses live limits from daemon metadata and preserves known model metadata', async () => {
|
||||||
|
const server = http.createServer((_req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4.5',
|
||||||
|
name: 'Claude Sonnet 4.5',
|
||||||
|
capabilities: {
|
||||||
|
limits: {
|
||||||
|
max_context_window_tokens: 128000,
|
||||||
|
max_prompt_tokens: 128000,
|
||||||
|
max_output_tokens: 64000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4.6',
|
||||||
|
name: 'Claude Sonnet 4.6',
|
||||||
|
capabilities: {
|
||||||
|
limits: {
|
||||||
|
max_context_window_tokens: 128000,
|
||||||
|
max_prompt_tokens: 128000,
|
||||||
|
max_output_tokens: 64000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
name: 'invalid-entry',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
throw new Error('Unable to resolve test server port');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const models = await fetchModelsFromDaemon(address.port);
|
||||||
|
expect(models).toHaveLength(2);
|
||||||
|
|
||||||
|
const knownModel = models.find((model) => model.id === 'claude-sonnet-4.5');
|
||||||
|
expect(knownModel?.provider).toBe('anthropic');
|
||||||
|
expect(knownModel?.minPlan).toBe('pro');
|
||||||
|
expect(knownModel?.multiplier).toBe(1);
|
||||||
|
expect(knownModel?.limits).toEqual({
|
||||||
|
maxContextWindowTokens: 128000,
|
||||||
|
maxPromptTokens: 128000,
|
||||||
|
maxOutputTokens: 64000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const liveOnlyModel = models.find((model) => model.id === 'claude-sonnet-4.6');
|
||||||
|
expect(liveOnlyModel?.name).toBe('Claude Sonnet 4.6');
|
||||||
|
expect(liveOnlyModel?.provider).toBe('anthropic');
|
||||||
|
expect(liveOnlyModel?.limits?.maxPromptTokens).toBe(128000);
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,35 @@ import { FREE_PRESETS, PAID_PRESETS } from './presets';
|
|||||||
import { FlexibleModelSelector } from './model-selector';
|
import { FlexibleModelSelector } from './model-selector';
|
||||||
import type { ModelPreset } from './types';
|
import type { ModelPreset } from './types';
|
||||||
|
|
||||||
|
function formatCompactTokens(value: number): string {
|
||||||
|
if (value >= 1_000_000) {
|
||||||
|
const millions = value / 1_000_000;
|
||||||
|
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
if (value >= 1_000) {
|
||||||
|
const thousands = value / 1_000;
|
||||||
|
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatModelLimits(model?: CopilotModel): string | null {
|
||||||
|
if (!model?.limits) return null;
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (model.limits.maxPromptTokens) {
|
||||||
|
parts.push(`prompt ${formatCompactTokens(model.limits.maxPromptTokens)}`);
|
||||||
|
}
|
||||||
|
if (model.limits.maxContextWindowTokens) {
|
||||||
|
parts.push(`context ${formatCompactTokens(model.limits.maxContextWindowTokens)}`);
|
||||||
|
}
|
||||||
|
if (model.limits.maxOutputTokens) {
|
||||||
|
parts.push(`output ${formatCompactTokens(model.limits.maxOutputTokens)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(' | ') : null;
|
||||||
|
}
|
||||||
|
|
||||||
interface ModelConfigTabProps {
|
interface ModelConfigTabProps {
|
||||||
currentModel: string;
|
currentModel: string;
|
||||||
opusModel: string;
|
opusModel: string;
|
||||||
@@ -41,6 +70,19 @@ export function ModelConfigTab({
|
|||||||
onUpdateSonnetModel,
|
onUpdateSonnetModel,
|
||||||
onUpdateHaikuModel,
|
onUpdateHaikuModel,
|
||||||
}: ModelConfigTabProps) {
|
}: ModelConfigTabProps) {
|
||||||
|
const mappedModelLimits = [
|
||||||
|
{ label: 'Default', id: currentModel },
|
||||||
|
{ label: 'Opus', id: opusModel || currentModel },
|
||||||
|
{ label: 'Sonnet', id: sonnetModel || currentModel },
|
||||||
|
{ label: 'Haiku', id: haikuModel || currentModel },
|
||||||
|
]
|
||||||
|
.map(({ label, id }) => {
|
||||||
|
const model = models.find((entry) => entry.id === id);
|
||||||
|
const limits = formatModelLimits(model);
|
||||||
|
return limits ? { label, id, limits } : null;
|
||||||
|
})
|
||||||
|
.filter((entry): entry is { label: string; id: string; limits: string } => entry !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="config"
|
value="config"
|
||||||
@@ -125,6 +167,26 @@ export function ModelConfigTab({
|
|||||||
<p className="text-xs text-muted-foreground mb-4">
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
Configure which models to use for each tier
|
Configure which models to use for each tier
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mb-4 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-900 dark:text-amber-200">
|
||||||
|
<p className="font-medium">GitHub Copilot controls prompt/context limits upstream.</p>
|
||||||
|
<p className="mt-1">
|
||||||
|
CCS can switch Copilot models, but it cannot increase the provider's max prompt
|
||||||
|
or context window.
|
||||||
|
</p>
|
||||||
|
{mappedModelLimits.length > 0 ? (
|
||||||
|
<div className="mt-2 space-y-1 text-[11px] font-mono">
|
||||||
|
{mappedModelLimits.map((entry) => (
|
||||||
|
<p key={`${entry.label}-${entry.id}`}>
|
||||||
|
{entry.label}: {entry.id} ({entry.limits})
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 text-[11px] font-mono">
|
||||||
|
Start the daemon to inspect live model limits from GitHub Copilot metadata.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FlexibleModelSelector
|
<FlexibleModelSelector
|
||||||
label="Default Model"
|
label="Default Model"
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ export interface CopilotConfig {
|
|||||||
/** GitHub Copilot plan tiers */
|
/** GitHub Copilot plan tiers */
|
||||||
export type CopilotPlanTier = 'free' | 'pro' | 'pro+' | 'business' | 'enterprise';
|
export type CopilotPlanTier = 'free' | 'pro' | 'pro+' | 'business' | 'enterprise';
|
||||||
|
|
||||||
|
export interface CopilotModelLimits {
|
||||||
|
maxContextWindowTokens?: number;
|
||||||
|
maxOutputTokens?: number;
|
||||||
|
maxPromptTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CopilotModel {
|
export interface CopilotModel {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -66,6 +72,8 @@ export interface CopilotModel {
|
|||||||
multiplier?: number;
|
multiplier?: number;
|
||||||
/** Whether this model is in preview */
|
/** Whether this model is in preview */
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
|
/** Live model limits returned by GitHub Copilot metadata, when available */
|
||||||
|
limits?: CopilotModelLimits;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotRawSettings {
|
export interface CopilotRawSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user