gitpt 1.4.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +7 -0
  2. package/dist/commands/commit/context/buildPrompt.d.ts +4 -0
  3. package/dist/commands/commit/context/buildPrompt.js +13 -0
  4. package/dist/commands/commit/context/summaryPrompt.d.ts +2 -0
  5. package/dist/commands/commit/context/summaryPrompt.js +17 -0
  6. package/dist/commands/commit/generateCommitMessage.js +8 -26
  7. package/dist/commands/commit/index.js +4 -2
  8. package/dist/commands/commit/summarizeDiff.d.ts +1 -0
  9. package/dist/commands/commit/summarizeDiff.js +172 -0
  10. package/dist/commands/middleware/setupMiddleware/defaultModels.d.ts +8 -0
  11. package/dist/commands/middleware/setupMiddleware/defaultModels.js +11 -0
  12. package/dist/commands/middleware/setupMiddleware/index.js +58 -24
  13. package/dist/commands/pr/generatePRDetails.js +7 -14
  14. package/dist/commands/reset.d.ts +3 -0
  15. package/dist/commands/reset.js +26 -0
  16. package/dist/config.d.ts +6 -10
  17. package/dist/config.js +25 -20
  18. package/dist/index.js +6 -0
  19. package/dist/llm/client.d.ts +24 -0
  20. package/dist/llm/index.d.ts +3 -2
  21. package/dist/llm/index.js +4 -9
  22. package/dist/llm/providers/anthropic/index.d.ts +9 -0
  23. package/dist/llm/providers/anthropic/index.js +31 -0
  24. package/dist/llm/providers/apiKey.d.ts +3 -0
  25. package/dist/llm/providers/apiKey.js +40 -0
  26. package/dist/llm/providers/apple/client.d.ts +3 -0
  27. package/dist/llm/providers/apple/client.js +87 -0
  28. package/dist/llm/providers/apple/index.d.ts +13 -0
  29. package/dist/llm/providers/apple/index.js +77 -0
  30. package/dist/llm/providers/apple/models.d.ts +14 -0
  31. package/dist/llm/providers/apple/models.js +21 -0
  32. package/dist/llm/providers/base.d.ts +30 -0
  33. package/dist/llm/providers/base.js +36 -0
  34. package/dist/llm/providers/local/index.d.ts +11 -0
  35. package/dist/llm/providers/local/index.js +96 -0
  36. package/dist/llm/providers/openai/index.d.ts +10 -0
  37. package/dist/llm/providers/openai/index.js +16 -0
  38. package/dist/llm/providers/openaiCompatible.d.ts +15 -0
  39. package/dist/llm/providers/openaiCompatible.js +69 -0
  40. package/dist/llm/providers/openrouter/index.d.ts +9 -0
  41. package/dist/llm/providers/openrouter/index.js +16 -0
  42. package/dist/llm/registry.d.ts +8 -0
  43. package/dist/llm/registry.js +43 -0
  44. package/dist/{commands/middleware/setupMiddleware → llm/setup}/getAvailableModels.d.ts +1 -0
  45. package/dist/{commands/middleware/setupMiddleware → llm/setup}/getAvailableModels.js +3 -3
  46. package/dist/{commands/middleware/setupMiddleware → llm/setup}/selectModel.d.ts +1 -1
  47. package/dist/{commands/middleware/setupMiddleware → llm/setup}/selectModel.js +13 -3
  48. package/dist/llm/setup/types.js +1 -0
  49. package/dist/llm/tokenCount.d.ts +3 -0
  50. package/dist/llm/tokenCount.js +4 -0
  51. package/dist/services/git/getStagedChanges.js +1 -1
  52. package/package.json +6 -2
  53. package/dist/commands/middleware/setupMiddleware/getOrUpdateApiKey.d.ts +0 -1
  54. package/dist/commands/middleware/setupMiddleware/getOrUpdateApiKey.js +0 -39
  55. package/dist/commands/middleware/setupMiddleware/setupLocalLLM.d.ts +0 -5
  56. package/dist/commands/middleware/setupMiddleware/setupLocalLLM.js +0 -60
  57. package/dist/commands/middleware/setupMiddleware/setupOpenRouter.d.ts +0 -2
  58. package/dist/commands/middleware/setupMiddleware/setupOpenRouter.js +0 -66
  59. /package/dist/{commands/middleware/setupMiddleware/types.js → llm/client.js} +0 -0
  60. /package/dist/{commands/middleware/setupMiddleware → llm/setup}/types.d.ts +0 -0
package/dist/llm/index.js CHANGED
@@ -1,14 +1,9 @@
1
1
  import openai from "openai";
2
- import { getConfig } from "../config.js";
3
2
  import { formatBaseURL } from "../utils/formatBaseURL.js";
4
3
  export const OPENROUTER_API_URL = "https://openrouter.ai/api/v1";
5
4
  export const getLLMClient = (options) => {
6
- const { baseURLOverride } = options || {};
7
- const { apiKey, customLLMEndpoint, provider } = getConfig();
8
- const localLLMEndpoint = provider === "local" ? customLLMEndpoint : undefined;
9
- const baseURL = formatBaseURL(baseURLOverride ?? localLLMEndpoint ?? OPENROUTER_API_URL);
10
- return new openai.OpenAI({
11
- apiKey,
12
- baseURL,
13
- });
5
+ const baseURL = formatBaseURL(options?.baseURLOverride ?? OPENROUTER_API_URL);
6
+ // The OpenAI SDK requires a non-empty key even when the server ignores it.
7
+ const apiKey = options?.apiKey || "not-needed";
8
+ return new openai.OpenAI({ apiKey, baseURL });
14
9
  };
@@ -0,0 +1,9 @@
1
+ import { GitPTConfig } from "../../../config.js";
2
+ import { OpenAICompatibleProvider } from "../openaiCompatible.js";
3
+ export declare class AnthropicProvider extends OpenAICompatibleProvider {
4
+ static readonly id = "anthropic";
5
+ static readonly label = "Anthropic";
6
+ static readonly baseURL = "https://api.anthropic.com/v1";
7
+ static setup(existingConfig: GitPTConfig): Promise<GitPTConfig>;
8
+ protected baseURL(): string;
9
+ }
@@ -0,0 +1,31 @@
1
+ import { OpenAICompatibleProvider, setupApiKeyProvider, } from "../openaiCompatible.js";
2
+ const ANTHROPIC_VERSION = "2023-06-01";
3
+ // Anthropic's /v1/models uses x-api-key auth (not the OpenAI SDK's bearer token).
4
+ const listAnthropicModels = async (apiKey) => {
5
+ const response = await fetch("https://api.anthropic.com/v1/models?limit=100", {
6
+ headers: { "x-api-key": apiKey, "anthropic-version": ANTHROPIC_VERSION },
7
+ });
8
+ if (!response.ok) {
9
+ throw new Error(`${response.status} ${await response.text()}`);
10
+ }
11
+ const data = (await response.json())?.data ?? [];
12
+ return data.map((m) => ({
13
+ id: m.id,
14
+ name: m.display_name,
15
+ }));
16
+ };
17
+ export class AnthropicProvider extends OpenAICompatibleProvider {
18
+ static id = "anthropic";
19
+ static label = "Anthropic";
20
+ static baseURL = "https://api.anthropic.com/v1";
21
+ static setup(existingConfig) {
22
+ return setupApiKeyProvider(existingConfig, {
23
+ baseURL: AnthropicProvider.baseURL,
24
+ label: AnthropicProvider.label,
25
+ listModels: listAnthropicModels,
26
+ });
27
+ }
28
+ baseURL() {
29
+ return AnthropicProvider.baseURL;
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ export declare const getApiKey: () => string | undefined;
2
+ export declare const saveApiKey: (providerId: string, key: string) => void;
3
+ export declare const promptApiKey: (existingKey?: string, label?: string) => Promise<string>;
@@ -0,0 +1,40 @@
1
+ import inquirer from "inquirer";
2
+ import { getConfig, saveConfig } from "../../config.js";
3
+ import { maskApiKey } from "../../utils/maskApiKey.js";
4
+ export const getApiKey = () => {
5
+ const { provider, apiKeys, apiKey } = getConfig();
6
+ return (provider && apiKeys?.[provider]) || apiKey;
7
+ };
8
+ export const saveApiKey = (providerId, key) => {
9
+ const { apiKeys } = getConfig();
10
+ saveConfig({ apiKeys: { ...apiKeys, [providerId]: key } });
11
+ };
12
+ export const promptApiKey = async (existingKey, label = "the provider") => {
13
+ if (existingKey) {
14
+ const { useExistingKey } = await inquirer.prompt([
15
+ {
16
+ type: "list",
17
+ name: "useExistingKey",
18
+ message: `${label} API key:`,
19
+ choices: [
20
+ {
21
+ name: `Use existing key (${maskApiKey(existingKey)})`,
22
+ value: true,
23
+ },
24
+ { name: "Enter a new API key", value: false },
25
+ ],
26
+ },
27
+ ]);
28
+ if (useExistingKey)
29
+ return existingKey;
30
+ }
31
+ const { apiKey } = await inquirer.prompt([
32
+ {
33
+ type: "input",
34
+ name: "apiKey",
35
+ message: `Enter your ${label} API key:`,
36
+ validate: (input) => (input ? true : "API key is required"),
37
+ },
38
+ ]);
39
+ return apiKey;
40
+ };
@@ -0,0 +1,3 @@
1
+ import type { LLMClient } from "../../client.js";
2
+ export declare const FM_BINARY = "fm";
3
+ export declare const getAppleFoundationClient: () => LLMClient;
@@ -0,0 +1,87 @@
1
+ import { spawn } from "child_process";
2
+ export const FM_BINARY = "fm";
3
+ const clean = (text) => text
4
+ .replace(/\x1b\[[0-9;]*m/g, "")
5
+ .replace(/[⠀-⣿]/g, "")
6
+ .trim();
7
+ const messageText = (content) => {
8
+ if (typeof content === "string")
9
+ return content;
10
+ if (Array.isArray(content)) {
11
+ return content
12
+ .map((part) => (part.type === "text" ? part.text : ""))
13
+ .join("");
14
+ }
15
+ return "";
16
+ };
17
+ const runFm = (args, stdin) => new Promise((resolve, reject) => {
18
+ const child = spawn(FM_BINARY, args, {
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ });
21
+ let stdout = "";
22
+ let stderr = "";
23
+ child.stdout.on("data", (chunk) => {
24
+ stdout += chunk.toString();
25
+ });
26
+ child.stderr.on("data", (chunk) => {
27
+ stderr += chunk.toString();
28
+ });
29
+ child.on("error", (error) => {
30
+ if (error.code === "ENOENT") {
31
+ reject(new Error("The Apple Foundation Models CLI ('fm') was not found. It ships with macOS 27 and later."));
32
+ return;
33
+ }
34
+ reject(error);
35
+ });
36
+ child.on("close", (code) => {
37
+ const out = stdout.trim();
38
+ const err = clean(stderr);
39
+ if (code !== 0 || !out) {
40
+ reject(new Error(err || out || `'fm' exited with code ${code} and no output`));
41
+ return;
42
+ }
43
+ resolve(out);
44
+ });
45
+ child.stdin.on("error", () => { });
46
+ child.stdin.write(stdin);
47
+ child.stdin.end();
48
+ });
49
+ export const getAppleFoundationClient = () => {
50
+ return {
51
+ chat: {
52
+ completions: {
53
+ create: async (params) => {
54
+ const model = params.model || "system";
55
+ const instructions = params.messages
56
+ .filter((m) => m.role === "system")
57
+ .map((m) => messageText(m.content))
58
+ .join("\n\n")
59
+ .trim();
60
+ const prompt = params.messages
61
+ .filter((m) => m.role !== "system")
62
+ .map((m) => messageText(m.content))
63
+ .join("\n\n")
64
+ .trim();
65
+ const args = ["respond", "--no-stream", "--model", model];
66
+ if (instructions) {
67
+ args.push("--instructions", instructions);
68
+ }
69
+ const content = await runFm(args, prompt);
70
+ return { choices: [{ message: { content } }] };
71
+ },
72
+ },
73
+ },
74
+ models: {
75
+ list: async () => ({
76
+ data: [
77
+ { id: "system", created: 0, object: "model", owned_by: "apple" },
78
+ { id: "pcc", created: 0, object: "model", owned_by: "apple" },
79
+ ],
80
+ hasNextPage: () => false,
81
+ getNextPage: async () => {
82
+ throw new Error("No more pages");
83
+ },
84
+ }),
85
+ },
86
+ };
87
+ };
@@ -0,0 +1,13 @@
1
+ import { GitPTConfig } from "../../../config.js";
2
+ import type { LLMClient } from "../../client.js";
3
+ import { Provider } from "../base.js";
4
+ export declare class AppleProvider extends Provider {
5
+ static readonly id = "apple";
6
+ static readonly label = "Apple Foundation Models (macOS 27+)";
7
+ static isAvailable(): boolean;
8
+ static setup(existingConfig: GitPTConfig): Promise<GitPTConfig>;
9
+ constructor(model: string);
10
+ getContextWindow(): Promise<number>;
11
+ countTokens(text: string): number;
12
+ protected getClient(): LLMClient;
13
+ }
@@ -0,0 +1,77 @@
1
+ import { spawnSync } from "child_process";
2
+ import { platform } from "os";
3
+ import chalk from "chalk";
4
+ import { saveConfig } from "../../../config.js";
5
+ import { selectModel } from "../../setup/selectModel.js";
6
+ import { Provider } from "../base.js";
7
+ import { getAppleFoundationClient } from "./client.js";
8
+ import { CANDIDATE_MODELS, probeModel } from "./models.js";
9
+ const MIN_MACOS_MAJOR = 27;
10
+ export class AppleProvider extends Provider {
11
+ static id = "apple";
12
+ static label = "Apple Foundation Models (macOS 27+)";
13
+ static isAvailable() {
14
+ if (platform() !== "darwin")
15
+ return false;
16
+ const result = spawnSync("sw_vers", ["-productVersion"], {
17
+ encoding: "utf-8",
18
+ });
19
+ if (result.error || !result.stdout)
20
+ return false;
21
+ const major = parseInt(result.stdout.trim().split(".")[0], 10);
22
+ return Number.isFinite(major) && major >= MIN_MACOS_MAJOR;
23
+ }
24
+ static async setup(existingConfig) {
25
+ console.log(chalk.blue("Apple Foundation Models Setup"));
26
+ const probes = CANDIDATE_MODELS.map((model) => ({
27
+ model,
28
+ ...probeModel(model.id),
29
+ }));
30
+ if (probes.every((p) => p.reason === "fm-missing")) {
31
+ console.error(chalk.red("The Apple Foundation Models CLI ('fm') was not found. It ships with macOS 27 and later."));
32
+ process.exit(1);
33
+ }
34
+ const availableModels = probes
35
+ .filter((p) => p.available)
36
+ .map((p) => p.model);
37
+ if (availableModels.length === 0) {
38
+ console.error(chalk.red("No Apple Foundation Models are available in this context. Make sure Apple Intelligence is enabled in System Settings."));
39
+ process.exit(1);
40
+ }
41
+ const notes = probes
42
+ .filter((p) => !p.available)
43
+ .map((p) => `Note: Apple doesn't allow ${p.model.name} access from command-line tools.`);
44
+ const selectedModel = await selectModel(availableModels, existingConfig.model, notes);
45
+ const updatedConfig = {
46
+ ...existingConfig,
47
+ provider: "apple",
48
+ model: selectedModel,
49
+ };
50
+ saveConfig(updatedConfig);
51
+ console.log(chalk.green(`✓ Model set to: ${chalk.yellow(selectedModel)}`));
52
+ return updatedConfig;
53
+ }
54
+ constructor(model) {
55
+ super(model || "system");
56
+ }
57
+ async getContextWindow() {
58
+ // The Apple Foundation Model CLI does not provide a way to get the context window size.
59
+ // `fm` only enables `system` model for programmatic use, the context window is always 4096 tokens.
60
+ return 4096;
61
+ }
62
+ countTokens(text) {
63
+ const result = spawnSync("fm", ["token-count", "-q"], {
64
+ input: text,
65
+ encoding: "utf-8",
66
+ });
67
+ if (!result.error && result.stdout) {
68
+ const tokens = parseInt(result.stdout.replace(/[^0-9]/g, ""), 10);
69
+ if (Number.isFinite(tokens))
70
+ return tokens;
71
+ }
72
+ return super.countTokens(text);
73
+ }
74
+ getClient() {
75
+ return getAppleFoundationClient();
76
+ }
77
+ }
@@ -0,0 +1,14 @@
1
+ export declare const CANDIDATE_MODELS: ({
2
+ id: string;
3
+ name: string;
4
+ context_length: number;
5
+ } | {
6
+ id: string;
7
+ name: string;
8
+ context_length?: undefined;
9
+ })[];
10
+ export declare const probeModel: (modelId: string) => {
11
+ available: boolean;
12
+ reason?: string;
13
+ };
14
+ export declare const isAppleModelAvailable: (modelId: string) => boolean;
@@ -0,0 +1,21 @@
1
+ import { spawnSync } from "child_process";
2
+ export const CANDIDATE_MODELS = [
3
+ {
4
+ id: "system",
5
+ name: "Apple On-Device Foundation Model",
6
+ context_length: 4096,
7
+ },
8
+ { id: "pcc", name: "Private Cloud Compute (PCC)" },
9
+ ];
10
+ export const probeModel = (modelId) => {
11
+ const result = spawnSync("fm", ["available", "--model", modelId], {
12
+ encoding: "utf-8",
13
+ });
14
+ if (result.error) {
15
+ return { available: false, reason: "fm-missing" };
16
+ }
17
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
18
+ const available = /available/i.test(output) && !/not available|error/i.test(output);
19
+ return { available, reason: output.trim() };
20
+ };
21
+ export const isAppleModelAvailable = (modelId) => probeModel(modelId).available;
@@ -0,0 +1,30 @@
1
+ import type { GitPTConfig } from "../../config.js";
2
+ import type { LLMClient } from "../client.js";
3
+ export interface CompletionRequest {
4
+ system: string;
5
+ user: string;
6
+ maxTokens: number;
7
+ }
8
+ export declare abstract class Provider {
9
+ readonly model: string;
10
+ static readonly requiresApiKey: boolean;
11
+ static readonly requiresEndpoint: boolean;
12
+ static isAvailable(): boolean;
13
+ readonly maxOutputTokens: number;
14
+ protected readonly usesCompletionTokensParam: boolean;
15
+ constructor(model: string);
16
+ getContextWindow(): Promise<number>;
17
+ countTokens(text: string): number;
18
+ protected abstract getClient(): LLMClient;
19
+ complete(req: CompletionRequest): Promise<string>;
20
+ }
21
+ export type ProviderClass = {
22
+ new (model: string): Provider;
23
+ readonly id: string;
24
+ readonly label: string;
25
+ readonly requiresApiKey: boolean;
26
+ readonly requiresEndpoint: boolean;
27
+ readonly baseURL?: string;
28
+ isAvailable(): boolean;
29
+ setup(existingConfig: GitPTConfig): Promise<GitPTConfig>;
30
+ };
@@ -0,0 +1,36 @@
1
+ export class Provider {
2
+ model;
3
+ static requiresApiKey = false;
4
+ static requiresEndpoint = false;
5
+ static isAvailable() {
6
+ return true;
7
+ }
8
+ maxOutputTokens = 1024;
9
+ // Newer OpenAI models require `max_completion_tokens`; most others use `max_tokens`.
10
+ usesCompletionTokensParam = false;
11
+ constructor(model) {
12
+ this.model = model;
13
+ }
14
+ async getContextWindow() {
15
+ return Number.POSITIVE_INFINITY;
16
+ }
17
+ countTokens(text) {
18
+ // Conservative estimate (code tokenizes higher than the usual ~4 chars/token),
19
+ // so chunks stay under the real context window when no exact tokenizer exists.
20
+ return Math.ceil(text.length / 3);
21
+ }
22
+ async complete(req) {
23
+ const tokenLimit = this.usesCompletionTokensParam
24
+ ? { max_completion_tokens: req.maxTokens }
25
+ : { max_tokens: req.maxTokens };
26
+ const response = await this.getClient().chat.completions.create({
27
+ model: this.model,
28
+ messages: [
29
+ { role: "system", content: req.system },
30
+ { role: "user", content: req.user },
31
+ ],
32
+ ...tokenLimit,
33
+ });
34
+ return response.choices[0].message.content ?? "";
35
+ }
36
+ }
@@ -0,0 +1,11 @@
1
+ import { GitPTConfig } from "../../../config.js";
2
+ import { OpenAICompatibleProvider } from "../openaiCompatible.js";
3
+ export declare class LocalProvider extends OpenAICompatibleProvider {
4
+ static readonly id = "local";
5
+ static readonly label = "Local LLM";
6
+ static readonly requiresApiKey = false;
7
+ static readonly requiresEndpoint = true;
8
+ static setup(existingConfig: GitPTConfig): Promise<GitPTConfig>;
9
+ getContextWindow(): Promise<number>;
10
+ protected baseURL(): string;
11
+ }
@@ -0,0 +1,96 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import { getConfig, saveConfig } from "../../../config.js";
4
+ import { getAvailableModels } from "../../setup/getAvailableModels.js";
5
+ import { selectModel } from "../../setup/selectModel.js";
6
+ import { OpenAICompatibleProvider } from "../openaiCompatible.js";
7
+ const DEFAULT_CONTEXT_WINDOW = 4096;
8
+ const detectContextWindow = async (endpoint, modelId) => {
9
+ try {
10
+ const origin = new URL(endpoint).origin;
11
+ const response = await fetch(`${origin}/api/v0/models`);
12
+ if (!response.ok)
13
+ return undefined;
14
+ const models = (await response.json())?.data ?? [];
15
+ const model = models.find((m) => m.id === modelId) ??
16
+ models.find((m) => m.state === "loaded");
17
+ const window = model?.loaded_context_length ?? model?.max_context_length;
18
+ return typeof window === "number" && window > 0 ? window : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ };
24
+ export class LocalProvider extends OpenAICompatibleProvider {
25
+ static id = "local";
26
+ static label = "Local LLM";
27
+ static requiresApiKey = false;
28
+ static requiresEndpoint = true;
29
+ static async setup(existingConfig) {
30
+ console.log(chalk.blue("Local LLM Setup"));
31
+ const endpointAnswer = await inquirer.prompt([
32
+ {
33
+ type: "input",
34
+ name: "localLLMEndpoint",
35
+ message: "Enter local LLM API endpoint (e.g., http://127.0.0.1:1234):",
36
+ default: existingConfig.customLLMEndpoint || "http://127.0.0.1:1234",
37
+ validate: (input) => {
38
+ if (!input)
39
+ return "API endpoint is required";
40
+ if (!input.startsWith("http://") && !input.startsWith("https://")) {
41
+ return "Must be a valid URL starting with http:// or https://";
42
+ }
43
+ return true;
44
+ },
45
+ },
46
+ ]);
47
+ const endpoint = endpointAnswer.localLLMEndpoint;
48
+ console.log(chalk.gray("Trying to fetch available models from local LLM server..."));
49
+ const models = await getAvailableModels({ baseURLOverride: endpoint });
50
+ let selectedModel;
51
+ if (models.length > 0) {
52
+ console.log(chalk.green(`✓ Found ${models.length} models available on your local LLM server`));
53
+ selectedModel = await selectModel(models, existingConfig.model);
54
+ }
55
+ else {
56
+ console.log(chalk.yellow("Could not fetch models from local LLM server, please enter model name manually"));
57
+ const modelAnswer = await inquirer.prompt([
58
+ {
59
+ type: "input",
60
+ name: "model",
61
+ message: "Enter model name to use with local endpoint:",
62
+ default: existingConfig.model,
63
+ validate: (input) => input ? true : "Model name is required",
64
+ },
65
+ ]);
66
+ selectedModel = modelAnswer.model;
67
+ }
68
+ const contextWindow = (await detectContextWindow(endpoint, selectedModel)) ??
69
+ DEFAULT_CONTEXT_WINDOW;
70
+ console.log(chalk.gray(`Using a context window of ${contextWindow} tokens.`));
71
+ const updatedConfig = {
72
+ ...existingConfig,
73
+ provider: "local",
74
+ model: selectedModel,
75
+ customLLMEndpoint: endpoint,
76
+ contextWindow,
77
+ };
78
+ saveConfig(updatedConfig);
79
+ console.log(chalk.green("✓ Local LLM configuration saved"));
80
+ return updatedConfig;
81
+ }
82
+ async getContextWindow() {
83
+ const { contextWindow, customLLMEndpoint, model } = getConfig();
84
+ if (contextWindow)
85
+ return contextWindow;
86
+ const detected = await detectContextWindow(customLLMEndpoint ?? "", model ?? "");
87
+ if (detected) {
88
+ saveConfig({ contextWindow: detected });
89
+ return detected;
90
+ }
91
+ return DEFAULT_CONTEXT_WINDOW;
92
+ }
93
+ baseURL() {
94
+ return getConfig().customLLMEndpoint ?? "";
95
+ }
96
+ }
@@ -0,0 +1,10 @@
1
+ import { GitPTConfig } from "../../../config.js";
2
+ import { OpenAICompatibleProvider } from "../openaiCompatible.js";
3
+ export declare class OpenAIProvider extends OpenAICompatibleProvider {
4
+ static readonly id = "openai";
5
+ static readonly label = "OpenAI";
6
+ static readonly baseURL = "https://api.openai.com/v1";
7
+ protected readonly usesCompletionTokensParam = true;
8
+ static setup(existingConfig: GitPTConfig): Promise<GitPTConfig>;
9
+ protected baseURL(): string;
10
+ }
@@ -0,0 +1,16 @@
1
+ import { OpenAICompatibleProvider, setupApiKeyProvider, } from "../openaiCompatible.js";
2
+ export class OpenAIProvider extends OpenAICompatibleProvider {
3
+ static id = "openai";
4
+ static label = "OpenAI";
5
+ static baseURL = "https://api.openai.com/v1";
6
+ usesCompletionTokensParam = true;
7
+ static setup(existingConfig) {
8
+ return setupApiKeyProvider(existingConfig, {
9
+ baseURL: OpenAIProvider.baseURL,
10
+ label: OpenAIProvider.label,
11
+ });
12
+ }
13
+ baseURL() {
14
+ return OpenAIProvider.baseURL;
15
+ }
16
+ }
@@ -0,0 +1,15 @@
1
+ import { GitPTConfig } from "../../config.js";
2
+ import type { LLMClient } from "../client.js";
3
+ import { Model } from "../setup/types.js";
4
+ import { Provider } from "./base.js";
5
+ export declare abstract class OpenAICompatibleProvider extends Provider {
6
+ static readonly requiresApiKey: boolean;
7
+ readonly maxOutputTokens = 2048;
8
+ protected abstract baseURL(): string;
9
+ protected getClient(): LLMClient;
10
+ }
11
+ export declare const setupApiKeyProvider: (existingConfig: GitPTConfig, opts: {
12
+ baseURL: string;
13
+ label: string;
14
+ listModels?: (apiKey: string) => Promise<Model[]>;
15
+ }) => Promise<GitPTConfig>;
@@ -0,0 +1,69 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import { saveConfig } from "../../config.js";
4
+ import { getLLMClient } from "../index.js";
5
+ import { getAvailableModels } from "../setup/getAvailableModels.js";
6
+ import { selectModel } from "../setup/selectModel.js";
7
+ import { getApiKey, promptApiKey, saveApiKey } from "./apiKey.js";
8
+ import { Provider } from "./base.js";
9
+ export class OpenAICompatibleProvider extends Provider {
10
+ static requiresApiKey = true;
11
+ // Headroom for reasoning models, which spend output tokens "thinking"
12
+ // before emitting the answer.
13
+ maxOutputTokens = 2048;
14
+ getClient() {
15
+ return getLLMClient({
16
+ baseURLOverride: this.baseURL(),
17
+ apiKey: getApiKey(),
18
+ });
19
+ }
20
+ }
21
+ export const setupApiKeyProvider = async (existingConfig, opts) => {
22
+ const { baseURL, label } = opts;
23
+ const listModels = opts.listModels ??
24
+ ((key) => getAvailableModels({ baseURLOverride: baseURL, apiKey: key }));
25
+ const providerId = existingConfig.provider ?? "";
26
+ const existingKey = existingConfig.apiKeys?.[providerId] ?? existingConfig.apiKey;
27
+ const apiKey = await promptApiKey(existingKey, label);
28
+ if (!apiKey) {
29
+ console.error(chalk.red(`API key is required for ${label}.`));
30
+ process.exit(1);
31
+ }
32
+ if (existingConfig.model) {
33
+ console.log("Current model:", chalk.yellow(existingConfig.model));
34
+ console.log("");
35
+ }
36
+ let selectedModel;
37
+ try {
38
+ console.log(chalk.gray(`Fetching available models from ${label}...`));
39
+ const models = await listModels(apiKey);
40
+ if (models.length > 0) {
41
+ console.log(chalk.green(`✓ Found ${models.length} models available with your API key`));
42
+ }
43
+ else {
44
+ console.log(chalk.yellow(`No models found from ${label}. Please specify a model manually.`));
45
+ }
46
+ selectedModel = await selectModel(models, existingConfig.model);
47
+ }
48
+ catch (error) {
49
+ console.error(chalk.yellow(`Error fetching models: ${error}`));
50
+ const modelAnswer = await inquirer.prompt([
51
+ {
52
+ type: "input",
53
+ name: "model",
54
+ message: "Enter model identifier:",
55
+ validate: (input) => input ? true : "Model identifier is required",
56
+ },
57
+ ]);
58
+ selectedModel = modelAnswer.model;
59
+ }
60
+ saveApiKey(providerId, apiKey);
61
+ const finalConfig = {
62
+ ...existingConfig,
63
+ model: selectedModel,
64
+ apiKeys: { ...existingConfig.apiKeys, [providerId]: apiKey },
65
+ };
66
+ saveConfig(finalConfig);
67
+ console.log(chalk.green(`✓ Model set to: ${chalk.yellow(selectedModel)}`));
68
+ return finalConfig;
69
+ };
@@ -0,0 +1,9 @@
1
+ import { GitPTConfig } from "../../../config.js";
2
+ import { OpenAICompatibleProvider } from "../openaiCompatible.js";
3
+ export declare class OpenRouterProvider extends OpenAICompatibleProvider {
4
+ static readonly id = "openrouter";
5
+ static readonly label = "OpenRouter (remote)";
6
+ static readonly baseURL = "https://openrouter.ai/api/v1";
7
+ static setup(existingConfig: GitPTConfig): Promise<GitPTConfig>;
8
+ protected baseURL(): string;
9
+ }
@@ -0,0 +1,16 @@
1
+ import { OPENROUTER_API_URL } from "../../index.js";
2
+ import { OpenAICompatibleProvider, setupApiKeyProvider, } from "../openaiCompatible.js";
3
+ export class OpenRouterProvider extends OpenAICompatibleProvider {
4
+ static id = "openrouter";
5
+ static label = "OpenRouter (remote)";
6
+ static baseURL = OPENROUTER_API_URL;
7
+ static setup(existingConfig) {
8
+ return setupApiKeyProvider(existingConfig, {
9
+ baseURL: OpenRouterProvider.baseURL,
10
+ label: OpenRouterProvider.label,
11
+ });
12
+ }
13
+ baseURL() {
14
+ return OpenRouterProvider.baseURL;
15
+ }
16
+ }
@@ -0,0 +1,8 @@
1
+ import type { Provider, ProviderClass } from "./providers/base.js";
2
+ export declare const PROVIDERS: ProviderClass[];
3
+ export declare const getProviderClass: (id: string | undefined) => ProviderClass | undefined;
4
+ export declare const getProvider: () => Provider;
5
+ export declare const validateConfig: () => {
6
+ isValid: boolean;
7
+ errors: string[];
8
+ };