@gitkraken/ai-provider-tools 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +35 -0
- package/LICENSE +19 -0
- package/README.md +64 -2
- package/dist/config.d.ts +56 -0
- package/dist/config.js +1 -0
- package/dist/create-model.d.ts +15 -0
- package/dist/create-model.js +1 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.js +1 -0
- package/dist/generators/changelog.d.ts +18 -0
- package/dist/generators/changelog.js +1 -0
- package/dist/generators/commit-message.d.ts +15 -0
- package/dist/generators/commit-message.js +1 -0
- package/dist/generators/explain-changes.d.ts +15 -0
- package/dist/generators/explain-changes.js +1 -0
- package/dist/generators/index.d.ts +5 -0
- package/dist/generators/index.js +1 -0
- package/dist/generators/pull-request.d.ts +15 -0
- package/dist/generators/pull-request.js +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -0
- package/dist/mask-api-key.d.ts +8 -0
- package/dist/mask-api-key.js +1 -0
- package/dist/models.d.ts +20 -0
- package/dist/models.js +1 -0
- package/dist/ports/model.d.ts +50 -0
- package/dist/ports/model.js +1 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.js +1 -0
- package/dist/providers/byok.d.ts +12 -0
- package/dist/providers/byok.js +1 -0
- package/dist/providers/gitkraken-errors.d.ts +21 -0
- package/dist/providers/gitkraken-errors.js +1 -0
- package/dist/providers/gitkraken-stream.d.ts +29 -0
- package/dist/providers/gitkraken-stream.js +3 -0
- package/dist/providers/gitkraken-wire.d.ts +156 -0
- package/dist/providers/gitkraken-wire.js +1 -0
- package/dist/providers/gitkraken.d.ts +48 -0
- package/dist/providers/gitkraken.js +1 -0
- package/dist/run-prompt-completion.d.ts +30 -0
- package/dist/run-prompt-completion.js +1 -0
- package/dist/xml.d.ts +14 -0
- package/dist/xml.js +1 -0
- package/package.json +44 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.1] - 2026-10-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Initial release of `@gitkraken/ai-provider-tools`: `createModel` (a GitKraken-hosted or BYOK Anthropic/OpenAI/OpenRouter/Ollama text-generation port, assignable to both `@gitkraken/shared-tools`' `AiModelPort` and `@gitkraken/conflict-tools`' `ConflictModelPort`), `listModels`, `getPromptTemplate`, `runPromptCompletion`, `AiError` (including a `truncated_output` code porting Go's `TruncatedOutputError`/`AllowTruncatedOutput`, via `ModelGenerateParams`' `allowTruncatedOutput` option), and `maskApiKey`, ported from gkcli's `internal/ai` package (gitkraken/gkcli#1726).
|
|
14
|
+
- `runPromptCompletion(config, action, variables, options?)` is public: it runs any server-side prompt template by its task action name (a plain `string`; there is no `PromptAction` union).
|
|
15
|
+
- `./generators` subpath export with `generateCommitMessage`, `generatePullRequest`, `explainChanges`, `generateChangelog` and their result types and parsers; these are not re-exported from the package root.
|
|
16
|
+
- `config.telemetry` now emits `ai.generate` on success and reports thrown errors via `.exception`, from both the hosted and BYOK code paths.
|
|
17
|
+
- `ModelGenerateResult` carries optional `finishReason` (the new exported `AiFinishReason`), `rawFinishReason` and `model`, from both the hosted and BYOK providers.
|
|
18
|
+
- `runPromptCompletion` resolves to the new exported `PromptCompletionResult` (`text`, `usage?`, `finishReason?`, `rawFinishReason?`, `model?`, and on the hosted provider `task?` with the task `id`, `status` and `provider`) instead of a bare string, and each generator result carries the completion's `usage`.
|
|
19
|
+
- `AiError` exposes `status`, `providerErrorCode` and `providerErrorType`, the HTTP status and the raw `code`/`type` of the provider's error body.
|
|
20
|
+
- `ChatCompleteRequest` is exported and types `tools` as `AiToolDefinition[]`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Hosted chat requests no longer follow redirects, and malformed successful task, model-list,
|
|
25
|
+
prompt-template or non-streamed chat responses now fail with classified `AiError`s. Prompt
|
|
26
|
+
template 4xx failures other than 401/429 are non-retryable `invalid_request` errors.
|
|
27
|
+
- Hosted chat and task requests preserve an `AbortError` raised while obtaining the token.
|
|
28
|
+
- XML result parsing ignores comments and treats CDATA contents as literal text.
|
|
29
|
+
- Renamed the package from `@gitkraken/ai-tools` to `@gitkraken/ai-provider-tools` (directory `packages/ai-provider`) and `AiToolsConfig` to `AiProviderConfig`; the old name was never published.
|
|
30
|
+
- `AiModel`'s `providerName`, `modelName`, `maxInputTokens`, `maxOutputTokens` and `consumptionRateLabel` are optional and copied from the backend response without zero-value defaults, and `AiCreditsExhaustedDetails`' `used` and `limit` are optional when an explicit exhaustion code carries no counts.
|
|
31
|
+
- `generateCommitMessage` keeps the blank lines in `description` instead of collapsing them, and `explainChanges` trims its `title` and `body` like the other generators.
|
|
32
|
+
- Hosted POST requests send `Content-Type: application/json`.
|
|
33
|
+
- OpenRouter requests send `HTTP-Referer: https://www.gitkraken.com/` and take `X-Title` from `config.client.name` (`GitKraken` when omitted).
|
|
34
|
+
- 401 error messages no longer mention a CLI command.
|
|
35
|
+
- `AiToolDefinition`, `AiToolCall` and `ToolAwareMessage` are now re-exported from `@gitkraken/shared-tools`, and `ToolAwareGenerateParams` extends the shared shape with `allowTruncatedOutput`, replacing the hand-synced copy of conflict-tools' types.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Core Tools License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Axosoft, LLC dba GitKraken ("GitKraken")
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") may be compiled as part of GitKraken software products, including the gitkraken/vscode-gitlens open source project (collectively, the "GitKraken Products"), to the extent the Software is a required component of the GitKraken Products; provided, however, that the Software and its functionality may only be used if you (and any entity that you represent) have agreed to, and are in compliance with, the GitKraken End User License Agreement, available at https://gitkraken.com/eula (the "EULA"), or other agreement governing the use of the Software, as agreed by you and GitKraken, and otherwise have a valid subscription for the correct number of user seats for the applicable version of the Software (e.g., GitLens Free+, GitLens Pro, GitLens Teams, and GitLens Enterprise).
|
|
6
|
+
|
|
7
|
+
The Software is licensed, not sold. This license only gives you some rights to use the Software. GitKraken reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the Software only as expressly permitted in this license. In doing so, you must comply with any technical limitations in the Software that only allow you to use it in certain ways. You may not (i) work around any technical limitations in the Software, (ii) reverse engineer, decompile or disassemble the Software, or otherwise attempt to derive the source code for the Software, except and to the extent required by third party licensing terms governing use of certain open source components that may be included with the Software, (iii) remove, minimize, block or modify any notices of GitKraken or its suppliers in the Software, (iv) use the software in any way that is against the law, (v) host, share, publish, rent or lease the Software; (vi) distribute the Software as a stand-alone or integrated offering or combine it with any of your applications for others to use, (vii) use the Software other than in connection with validly licensed GitKraken Products or (viii) use any portion of the Software to create software with the same or similar functionality.
|
|
8
|
+
|
|
9
|
+
You agree that GitKraken and/or its licensors (as applicable) retain all right, title and interest in and to the Software and all modifications and/or patches thereto. You are not granted any other rights beyond what is expressly stated herein. Except as set forth above, it is forbidden to copy, merge, publish, distribute, sublicense, modify and/or sell the Software.
|
|
10
|
+
|
|
11
|
+
If you give feedback about the Software to GitKraken, you give to GitKraken, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires GitKraken to license its software or documentation to third parties because we include your feedback in them. These rights survive this License.
|
|
12
|
+
|
|
13
|
+
The full text of this Core Tools License shall be included in all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
This License and the EULA, if applicable, are the entire agreement for the Software. You must comply with all domestic and international export laws and regulations that apply to the Software, which include restrictions on destinations, end users and end use. This License is governed and construed by the laws of the State of Arizona, United States without regard to any choice or conflict of law principles or rules.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL GITKRAKEN, THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
18
|
+
|
|
19
|
+
For all third party components incorporated into the Software, those components are licensed under the original license provided by the owner of the applicable component.
|
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @gitkraken/ai-provider-tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI text generation for GitKraken client SDKs: the GitKraken-hosted backend and BYOK
|
|
4
|
+
providers (Anthropic, OpenAI, OpenRouter, Ollama) behind one model port.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @gitkraken/ai-provider-tools
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createModel } from '@gitkraken/ai-provider-tools';
|
|
16
|
+
import { generateCommitMessage } from '@gitkraken/ai-provider-tools/generators';
|
|
17
|
+
|
|
18
|
+
const config = {
|
|
19
|
+
provider: 'gitkraken' as const,
|
|
20
|
+
getToken: async () => authStore.accessToken,
|
|
21
|
+
organizationId: currentOrg.id,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const model = createModel(config);
|
|
25
|
+
const { text } = await model.generate({ messages: [{ role: 'user', content: 'hi' }] });
|
|
26
|
+
|
|
27
|
+
const { message, description } = await generateCommitMessage(config, diff);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`runPromptCompletion(config, action, variables)` (root export) runs any server-side prompt template by its task action name and returns `{ text, usage?, finishReason?, rawFinishReason?, model? }`; the `./generators` subpath wraps it for commit messages, pull requests, change explanations and changelogs, each result carrying the completion's `usage` when the provider reports it.
|
|
31
|
+
|
|
32
|
+
A BYOK provider looks the same, minus the GitKraken-only fields:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const model = createModel({ provider: 'anthropic', apiKey, model: 'claude-sonnet-4-5' });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Model port
|
|
39
|
+
|
|
40
|
+
`createModel`'s result is a `Model` whose `generate` accepts either shared-tools'
|
|
41
|
+
`AiGenerateParams` or a tool-aware superset of it (system/messages/tools/maxTokens/
|
|
42
|
+
temperature/signal), and is assignable to both `@gitkraken/shared-tools`' `AiModelPort`
|
|
43
|
+
and `@gitkraken/conflict-tools`' `ConflictModelPort` — see `ARCHITECTURE.md` and
|
|
44
|
+
`src/model-port.type.test.ts`.
|
|
45
|
+
|
|
46
|
+
`generate` resolves to `{ text, toolCalls?, usage?, finishReason?, rawFinishReason?, model? }`.
|
|
47
|
+
`finishReason` is provider-neutral (`stop`, `length`, `tool-calls`, `content-filter`,
|
|
48
|
+
`error`, `other`), `rawFinishReason` is the provider's exact value, and `model` is the model
|
|
49
|
+
id the response reports.
|
|
50
|
+
|
|
51
|
+
## Errors
|
|
52
|
+
|
|
53
|
+
Every operation throws `AiError` on failure:
|
|
54
|
+
`{ code, message, retryable, status?, providerErrorCode?, providerErrorType?, details? }`.
|
|
55
|
+
`status` is the HTTP status, and `providerErrorCode`/`providerErrorType` are the raw
|
|
56
|
+
`code`/`type` of the provider's error body. `code` is one of `credits_exhausted`, `not_logged_in`, `rate_limited`,
|
|
57
|
+
`invalid_request`, `provider_error`, `truncated_output`. User aborts are rethrown
|
|
58
|
+
unchanged rather than wrapped. See `ARCHITECTURE.md` for the mapping.
|
|
59
|
+
|
|
60
|
+
## What the host owns
|
|
61
|
+
|
|
62
|
+
`getToken`, `fetch`, `request` (an `@gitkraken/api-tools` `ApiRequest`), `getCreditUsage`,
|
|
63
|
+
`onCreditUsage`, and `telemetry` are all injected. This package never reads
|
|
64
|
+
`process.env`, writes through a `StoragePort`, or caches a model list — see
|
|
65
|
+
`ARCHITECTURE.md`.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ApiRequest } from "@gitkraken/api-tools";
|
|
2
|
+
import { AiCreditUsage, GetAiCreditUsage, TelemetryPort } from "@gitkraken/shared-tools";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
type AiProviderId = 'gitkraken' | 'anthropic' | 'openai' | 'openrouter' | 'ollama';
|
|
6
|
+
/**
|
|
7
|
+
* Configuration shared by `createModel`, `listModels`, `getPromptTemplate` and
|
|
8
|
+
* the four `generate*` functions. Only the fields a given provider/operation
|
|
9
|
+
* needs are read; the rest are ignored. Hosted `generate` calls send
|
|
10
|
+
* `GK-Action: compose-changes`.
|
|
11
|
+
*/
|
|
12
|
+
interface AiProviderConfig {
|
|
13
|
+
provider: AiProviderId;
|
|
14
|
+
/** Model id. Falls back to the provider's own default when omitted. */
|
|
15
|
+
model?: string;
|
|
16
|
+
/** Required for BYOK providers other than Ollama. Ignored for `gitkraken`. */
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Overrides the provider's default endpoint. For `gitkraken`, this is the
|
|
20
|
+
* `v1/ai-tasks` API origin (default `https://api.gitkraken.dev/`), and it
|
|
21
|
+
* also controls where the prompt template is fetched from. For BYOK
|
|
22
|
+
* providers, this is the provider's own endpoint only — the prompt template
|
|
23
|
+
* is fetched from the `gitkraken` API origin, overridable via
|
|
24
|
+
* `gitkrakenApiBaseUrl`.
|
|
25
|
+
*/
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
/** GitKraken API origin used for prompt templates when provider is BYOK; defaults to https://api.gitkraken.dev/. */
|
|
28
|
+
gitkrakenApiBaseUrl?: string;
|
|
29
|
+
/** Supplies a fresh bearer token per request. Used for `gitkraken` requests, and for BYOK providers' prompt-template fetch. */
|
|
30
|
+
getToken?: () => Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* `gitkraken` only: the `GK-Org-Id` header value. The hosted chat-completions
|
|
33
|
+
* route rejects requests without it (HTTP 403).
|
|
34
|
+
*/
|
|
35
|
+
organizationId?: string;
|
|
36
|
+
/** `gitkraken`: sent as the `Client-Name`/`Client-Version` headers. `openrouter`: `name` also becomes OpenRouter's `X-Title` (default `GitKraken`). */
|
|
37
|
+
client?: {
|
|
38
|
+
name: string;
|
|
39
|
+
version: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Raw fetch, used for the `gitkraken` provider's streamed chat/completions
|
|
43
|
+
* call and injected into the BYOK AI SDK providers so the host controls the
|
|
44
|
+
* network. Defaults to `globalThis.fetch`.
|
|
45
|
+
*/
|
|
46
|
+
fetch?: typeof globalThis.fetch;
|
|
47
|
+
/** Used for the `gitkraken` non-streamed task route, and for BYOK providers' prompt-template fetch. Defaults to a `globalThis.fetch`-backed requester. */
|
|
48
|
+
request?: ApiRequest;
|
|
49
|
+
/** Looked up when a `gitkraken` request fails with 402/403/429 and no explicit credits code (D10). */
|
|
50
|
+
getCreditUsage?: GetAiCreditUsage;
|
|
51
|
+
/** Called with the credit usage reported alongside a successful `gitkraken` response, if any. The host decides whether/how to warn. */
|
|
52
|
+
onCreditUsage?: (usage: AiCreditUsage) => void;
|
|
53
|
+
telemetry?: TelemetryPort;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { AiProviderConfig, AiProviderId };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AiProviderConfig } from "./config.js";
|
|
2
|
+
import { Model } from "./ports/model.js";
|
|
3
|
+
|
|
4
|
+
//#region src/create-model.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Creates a text-generation `Model` for the configured provider: the
|
|
7
|
+
* GitKraken-hosted backend, or a BYOK provider (Anthropic, OpenAI, OpenRouter,
|
|
8
|
+
* Ollama) via the Vercel AI SDK. The result is assignable to both
|
|
9
|
+
* `@gitkraken/shared-tools`' `AiModelPort` and `@gitkraken/conflict-tools`'
|
|
10
|
+
* `ConflictModelPort` — see `model-port.type.test.ts`. Hosted `generate` calls
|
|
11
|
+
* send `GK-Action: compose-changes`.
|
|
12
|
+
*/
|
|
13
|
+
declare function createModel(config: AiProviderConfig): Model;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { createModel };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createByokModel as e}from"./providers/byok.js";import{createGitKrakenModel as t}from"./providers/gitkraken.js";function n(n){return n.provider===`gitkraken`?t(n):e(n)}export{n as createModel};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stable, machine-readable error codes. See D10 in the gkcli#1726 handoff
|
|
4
|
+
* plan. `truncated_output` is not in D10's table; it ports Go's
|
|
5
|
+
* `TruncatedOutputError` (a generic `ExpectedError` there, with no dedicated
|
|
6
|
+
* Go code) as its own code so a host can special-case it, e.g. by retrying
|
|
7
|
+
* with less input.
|
|
8
|
+
*/
|
|
9
|
+
type AiErrorCode = 'credits_exhausted' | 'not_logged_in' | 'rate_limited' | 'invalid_request' | 'provider_error' | 'truncated_output';
|
|
10
|
+
/** The credit snapshot attached to a `credits_exhausted` error's `details`. */
|
|
11
|
+
interface AiCreditsExhaustedDetails {
|
|
12
|
+
/** Absent when the explicit exhaustion code carried no usable count. */
|
|
13
|
+
used?: number;
|
|
14
|
+
/** Absent when the explicit exhaustion code carried no usable count. */
|
|
15
|
+
limit?: number;
|
|
16
|
+
resetsOn?: string;
|
|
17
|
+
organizationId?: string;
|
|
18
|
+
}
|
|
19
|
+
interface AiErrorOptions {
|
|
20
|
+
retryable?: boolean;
|
|
21
|
+
details?: unknown;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
status?: number;
|
|
24
|
+
providerErrorCode?: string;
|
|
25
|
+
providerErrorType?: string;
|
|
26
|
+
}
|
|
27
|
+
/** A classified AI request failure. `retryable` defaults per `code` unless overridden. */
|
|
28
|
+
declare class AiError extends Error {
|
|
29
|
+
readonly code: AiErrorCode;
|
|
30
|
+
readonly retryable: boolean;
|
|
31
|
+
readonly details?: unknown;
|
|
32
|
+
/** The HTTP status of the failed response, when the failure came from one. */
|
|
33
|
+
readonly status?: number;
|
|
34
|
+
/** The raw `code` field of the provider's error body, when it sent one. */
|
|
35
|
+
readonly providerErrorCode?: string;
|
|
36
|
+
/** The raw `type` field of the provider's error body, when it sent one. */
|
|
37
|
+
readonly providerErrorType?: string;
|
|
38
|
+
constructor(code: AiErrorCode, message: string, options?: AiErrorOptions);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reads the provider's own `code`/`type` from an error body (a JSON string or
|
|
42
|
+
* an already-parsed object), preferring the nested `error` object over the
|
|
43
|
+
* top level.
|
|
44
|
+
*/
|
|
45
|
+
declare function wireErrorFields(body: unknown): {
|
|
46
|
+
providerErrorCode?: string;
|
|
47
|
+
providerErrorType?: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Reports whether a provider's terminal reason means the output hit the
|
|
51
|
+
* token limit. Ports Go's `providers.IsTruncatedFinishReason`: OpenAI-
|
|
52
|
+
* compatible providers and Ollama say `"length"`, Anthropic says
|
|
53
|
+
* `"max_tokens"`. The Vercel AI SDK already normalizes every BYOK provider's
|
|
54
|
+
* raw reason to its own `"length"`, so `byok.ts` only ever passes that value
|
|
55
|
+
* here; the `"max_tokens"` case is kept so a caller checking a provider's raw
|
|
56
|
+
* (non-normalized) reason string gets the same answer Go would.
|
|
57
|
+
*/
|
|
58
|
+
declare function isTruncatedFinishReason(reason: string): boolean;
|
|
59
|
+
/** Ports Go's `providers.TruncatedOutputError`. */
|
|
60
|
+
declare function truncatedOutputError(): AiError;
|
|
61
|
+
declare function isAbortError(error: unknown): boolean;
|
|
62
|
+
/** Passes aborts and `AiError`s through unchanged; wraps anything else as a retryable `provider_error`. */
|
|
63
|
+
declare function toAiError(error: unknown, context: string): unknown;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { AiCreditsExhaustedDetails, AiError, AiErrorCode, isAbortError, isTruncatedFinishReason, toAiError, truncatedOutputError, wireErrorFields };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return e===`rate_limited`||e===`provider_error`}var t=class extends Error{code;retryable;details;status;providerErrorCode;providerErrorType;constructor(t,n,r){super(n,r?.cause===void 0?void 0:{cause:r.cause}),this.name=`AiError`,this.code=t,this.retryable=r?.retryable??e(t),this.details=r?.details,r?.status!==void 0&&(this.status=r.status),r?.providerErrorCode!==void 0&&(this.providerErrorCode=r.providerErrorCode),r?.providerErrorType!==void 0&&(this.providerErrorType=r.providerErrorType)}};function n(e,t){let n=e[t];return typeof n==`string`?n:void 0}function r(e){let t=e;if(typeof e==`string`)try{t=JSON.parse(e)}catch{t={}}if(typeof t!=`object`||!t)return{};let r=t,i=r.error!==null&&typeof r.error==`object`?r.error:void 0,a=(i&&n(i,`code`))??n(r,`code`),o=(i&&n(i,`type`))??n(r,`type`);return{...a===void 0?{}:{providerErrorCode:a},...o===void 0?{}:{providerErrorType:o}}}function i(e){return e===`length`||e===`max_tokens`}function a(){return new t(`truncated_output`,`AI provider returned an incomplete response because it hit the output token limit.`)}function o(e){return e instanceof Error&&e.name===`AbortError`}function s(e,n){return e instanceof t||o(e)?e:new t(`provider_error`,`${n}: ${e instanceof Error?e.message:String(e)}`,{cause:e})}export{t as AiError,o as isAbortError,i as isTruncatedFinishReason,s as toAiError,a as truncatedOutputError,r as wireErrorFields};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
|
+
|
|
4
|
+
//#region src/generators/changelog.d.ts
|
|
5
|
+
interface ChangelogResult {
|
|
6
|
+
body: string;
|
|
7
|
+
usage?: AiTokenUsage;
|
|
8
|
+
}
|
|
9
|
+
/** Ports Go's `GenerateChangelog` tail: the completion is used verbatim, not XML-parsed. */
|
|
10
|
+
declare function parseChangelogResult(content: string): ChangelogResult;
|
|
11
|
+
/**
|
|
12
|
+
* Ports Go's `GenerateChangelog`. A changelog is used verbatim, so a
|
|
13
|
+
* clipped-but-usable answer beats an error: this is the only generator that
|
|
14
|
+
* passes `allowTruncatedOutput: true`.
|
|
15
|
+
*/
|
|
16
|
+
declare function generateChangelog(config: AiProviderConfig, commits: string[]): Promise<ChangelogResult>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ChangelogResult, generateChangelog, parseChangelogResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{runPromptCompletion as e}from"../run-prompt-completion.js";function t(e){return{body:e.trim()}}async function n(n,r){let{text:i,usage:a}=await e(n,`generate-changelog`,{data:JSON.stringify(r)},{allowTruncatedOutput:!0});return{...t(i),...a?{usage:a}:{}}}export{n as generateChangelog,t as parseChangelogResult};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
|
+
|
|
4
|
+
//#region src/generators/commit-message.d.ts
|
|
5
|
+
interface CommitMessageResult {
|
|
6
|
+
message: string;
|
|
7
|
+
description: string;
|
|
8
|
+
usage?: AiTokenUsage;
|
|
9
|
+
}
|
|
10
|
+
/** Ports Go's `parseCommitMessageResponse`. Both fields are trimmed; blank lines inside `description` are kept. */
|
|
11
|
+
declare function parseCommitMessageResult(content: string): CommitMessageResult;
|
|
12
|
+
/** Ports Go's `GenerateCommitMessage`. A clipped completion is unusable XML, so it fails loudly (Go: `allowTruncated=false`). */
|
|
13
|
+
declare function generateCommitMessage(config: AiProviderConfig, diff: string): Promise<CommitMessageResult>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { CommitMessageResult, generateCommitMessage, parseCommitMessageResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{runPromptCompletion as e}from"../run-prompt-completion.js";import{parseXmlResult as t}from"../xml.js";function n(e){let{summary:n,body:r}=t(e);return{message:n.trim(),description:r.trim()}}async function r(t,r){let{text:i,usage:a}=await e(t,`generate-commitMessage`,{diff:r});return{...n(i),...a?{usage:a}:{}}}export{r as generateCommitMessage,n as parseCommitMessageResult};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
|
+
|
|
4
|
+
//#region src/generators/explain-changes.d.ts
|
|
5
|
+
interface ExplainChangesResult {
|
|
6
|
+
title: string;
|
|
7
|
+
body: string;
|
|
8
|
+
usage?: AiTokenUsage;
|
|
9
|
+
}
|
|
10
|
+
/** Ports the XML parsing inline in Go's `ExplainChanges`. */
|
|
11
|
+
declare function parseExplainChangesResult(content: string): ExplainChangesResult;
|
|
12
|
+
/** Ports Go's `ExplainChanges` (Go: `allowTruncated=false`). */
|
|
13
|
+
declare function explainChanges(config: AiProviderConfig, commitMessage: string, diff: string): Promise<ExplainChangesResult>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { ExplainChangesResult, explainChanges, parseExplainChangesResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{runPromptCompletion as e}from"../run-prompt-completion.js";import{parseXmlResult as t}from"../xml.js";function n(e){let{summary:n,body:r}=t(e);return{title:n.trim(),body:r.trim()}}async function r(t,r,i){let{text:a,usage:o}=await e(t,`explain-changes`,{diff:i,message:r});return{...n(a),...o?{usage:o}:{}}}export{r as explainChanges,n as parseExplainChangesResult};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ChangelogResult, generateChangelog, parseChangelogResult } from "./changelog.js";
|
|
2
|
+
import { CommitMessageResult, generateCommitMessage, parseCommitMessageResult } from "./commit-message.js";
|
|
3
|
+
import { ExplainChangesResult, explainChanges, parseExplainChangesResult } from "./explain-changes.js";
|
|
4
|
+
import { PullRequestResult, generatePullRequest, parsePullRequestResult } from "./pull-request.js";
|
|
5
|
+
export { type ChangelogResult, type CommitMessageResult, type ExplainChangesResult, type PullRequestResult, explainChanges, generateChangelog, generateCommitMessage, generatePullRequest, parseChangelogResult, parseCommitMessageResult, parseExplainChangesResult, parsePullRequestResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{generateChangelog as e,parseChangelogResult as t}from"./changelog.js";import{generateCommitMessage as n,parseCommitMessageResult as r}from"./commit-message.js";import{explainChanges as i,parseExplainChangesResult as a}from"./explain-changes.js";import{generatePullRequest as o,parsePullRequestResult as s}from"./pull-request.js";export{i as explainChanges,e as generateChangelog,n as generateCommitMessage,o as generatePullRequest,t as parseChangelogResult,r as parseCommitMessageResult,a as parseExplainChangesResult,s as parsePullRequestResult};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { AiTokenUsage } from "@gitkraken/shared-tools";
|
|
3
|
+
|
|
4
|
+
//#region src/generators/pull-request.d.ts
|
|
5
|
+
interface PullRequestResult {
|
|
6
|
+
title: string;
|
|
7
|
+
body: string;
|
|
8
|
+
usage?: AiTokenUsage;
|
|
9
|
+
}
|
|
10
|
+
/** Ports the XML parsing inline in Go's `GeneratePullRequestInfo`. */
|
|
11
|
+
declare function parsePullRequestResult(content: string): PullRequestResult;
|
|
12
|
+
/** Ports Go's `GeneratePullRequestInfo` (Go: `allowTruncated=false`). */
|
|
13
|
+
declare function generatePullRequest(config: AiProviderConfig, diff: string): Promise<PullRequestResult>;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { PullRequestResult, generatePullRequest, parsePullRequestResult };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{runPromptCompletion as e}from"../run-prompt-completion.js";import{parseXmlResult as t}from"../xml.js";function n(e){let{summary:n,body:r}=t(e);return{title:n.trim(),body:r.trim()}}async function r(t,r){let{text:i,usage:a}=await e(t,`generate-create-pullRequest`,{diff:r});return{...n(i),...a?{usage:a}:{}}}export{r as generatePullRequest,n as parsePullRequestResult};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AiProviderConfig, AiProviderId } from "./config.js";
|
|
2
|
+
import { AiFinishReason, AiToolCall, AiToolDefinition, Model, ModelGenerateParams, ModelGenerateResult, ToolAwareGenerateParams, ToolAwareMessage } from "./ports/model.js";
|
|
3
|
+
import { createModel } from "./create-model.js";
|
|
4
|
+
import { AiCreditsExhaustedDetails, AiError, AiErrorCode } from "./errors.js";
|
|
5
|
+
import { maskApiKey } from "./mask-api-key.js";
|
|
6
|
+
import { AiModel } from "./models.js";
|
|
7
|
+
import { getPromptTemplate } from "./prompts.js";
|
|
8
|
+
import { AiTaskInfo, ChatCompleteRequest, chatComplete, listModels } from "./providers/gitkraken.js";
|
|
9
|
+
import { PromptCompletionResult, RunPromptCompletionOptions, runPromptCompletion } from "./run-prompt-completion.js";
|
|
10
|
+
import { XmlResultContent, parseXmlResult } from "./xml.js";
|
|
11
|
+
export { type AiCreditsExhaustedDetails, AiError, type AiErrorCode, type AiFinishReason, type AiModel, type AiProviderConfig, type AiProviderId, type AiTaskInfo, type AiToolCall, type AiToolDefinition, type ChatCompleteRequest, type Model, type ModelGenerateParams, type ModelGenerateResult, type PromptCompletionResult, type RunPromptCompletionOptions, type ToolAwareGenerateParams, type ToolAwareMessage, type XmlResultContent, chatComplete, createModel, getPromptTemplate, listModels, maskApiKey, parseXmlResult, runPromptCompletion };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AiError as e}from"./errors.js";import{chatComplete as t,listModels as n}from"./providers/gitkraken.js";import{createModel as r}from"./create-model.js";import{maskApiKey as i}from"./mask-api-key.js";import{getPromptTemplate as a}from"./prompts.js";import{runPromptCompletion as o}from"./run-prompt-completion.js";import{parseXmlResult as s}from"./xml.js";export{e as AiError,t as chatComplete,r as createModel,a as getPromptTemplate,n as listModels,i as maskApiKey,s as parseXmlResult,o as runPromptCompletion};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/mask-api-key.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Redacts an API key to its last four characters, e.g. `sk-secret1234` ->
|
|
4
|
+
* `****1234`. A direct port of Go's `internal/actions/ai.MaskAPIKey`.
|
|
5
|
+
*/
|
|
6
|
+
declare function maskApiKey(key: string): string;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { maskApiKey };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return e.length<=4?`****`:`****${e.slice(-4)}`}export{e as maskApiKey};
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/models.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* One model the GitKraken AI backend offers for a task type. `modelId` is the
|
|
4
|
+
* `provider:model` string `createModel`'s `model` field accepts. Optional
|
|
5
|
+
* fields are copied from the backend's response and are absent when it omits
|
|
6
|
+
* them.
|
|
7
|
+
*/
|
|
8
|
+
interface AiModel {
|
|
9
|
+
providerId: string;
|
|
10
|
+
providerName?: string;
|
|
11
|
+
modelId: string;
|
|
12
|
+
modelName?: string;
|
|
13
|
+
maxInputTokens?: number;
|
|
14
|
+
maxOutputTokens?: number;
|
|
15
|
+
preferred: boolean;
|
|
16
|
+
/** The backend's "average token credit consumption rate" display label (e.g. `"4.5x"`). */
|
|
17
|
+
consumptionRateLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { AiModel };
|
package/dist/models.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AiGenerateParams, AiTokenUsage, AiToolCall, AiToolCall as AiToolCall$1, AiToolDefinition, ToolAwareGenerateParams as ToolAwareGenerateParams$1, ToolAwareMessage } from "@gitkraken/shared-tools";
|
|
2
|
+
|
|
3
|
+
//#region src/ports/model.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* shared-tools' canonical {@link SharedToolAwareGenerateParams} (which conflict-tools'
|
|
6
|
+
* `ConflictModelParams` aliases) plus `allowTruncatedOutput`, kept out of the shared shape so
|
|
7
|
+
* conflict-tools' port is untouched.
|
|
8
|
+
*/
|
|
9
|
+
interface ToolAwareGenerateParams extends ToolAwareGenerateParams$1 {
|
|
10
|
+
/**
|
|
11
|
+
* When false (the default), a completion cut short by the provider's
|
|
12
|
+
* output-token limit throws `AiError('truncated_output', ...)` instead of
|
|
13
|
+
* returning the partial text. Only BYOK providers honor this: the
|
|
14
|
+
* `gitkraken` provider's chat/completions route always errors on truncation.
|
|
15
|
+
*/
|
|
16
|
+
allowTruncatedOutput?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The union `Model.generate` accepts. A function typed to accept this union is
|
|
20
|
+
* assignable to both `AiModelPort.generate` (which requires `AiGenerateParams`)
|
|
21
|
+
* and `ConflictModelPort.generate` (which requires `ConflictModelParams`, the
|
|
22
|
+
* an alias of shared-tools' `ToolAwareGenerateParams`), because each of those
|
|
23
|
+
* narrower types is itself assignable to this union. `model-port.type.test.ts`
|
|
24
|
+
* checks this assignability directly against the real conflict-tools types.
|
|
25
|
+
*/
|
|
26
|
+
type ModelGenerateParams = AiGenerateParams | ToolAwareGenerateParams;
|
|
27
|
+
/** Provider-neutral reason a completion stopped. `rawFinishReason` carries the provider's exact value. */
|
|
28
|
+
type AiFinishReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error' | 'other';
|
|
29
|
+
/**
|
|
30
|
+
* `text` is always a string (possibly empty, e.g. a tool-calls-only turn)
|
|
31
|
+
* because `AiModelPort.generate` requires it; the optional fields make this
|
|
32
|
+
* assignable to `ConflictModelPort`'s result shape too.
|
|
33
|
+
*/
|
|
34
|
+
interface ModelGenerateResult {
|
|
35
|
+
text: string;
|
|
36
|
+
toolCalls?: AiToolCall$1[];
|
|
37
|
+
usage?: AiTokenUsage;
|
|
38
|
+
/** Provider-neutral reason the completion stopped. */
|
|
39
|
+
finishReason?: AiFinishReason;
|
|
40
|
+
/** The exact finish reason the provider reported, before normalization. */
|
|
41
|
+
rawFinishReason?: string;
|
|
42
|
+
/** The model id the provider's response reports, which may be more specific than the requested one. */
|
|
43
|
+
model?: string;
|
|
44
|
+
}
|
|
45
|
+
/** The port `createModel` returns. See {@link ModelGenerateParams}. */
|
|
46
|
+
interface Model {
|
|
47
|
+
generate(params: ModelGenerateParams): Promise<ModelGenerateResult>;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { AiFinishReason, type AiToolCall, type AiToolDefinition, Model, ModelGenerateParams, ModelGenerateResult, ToolAwareGenerateParams, type ToolAwareMessage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AiProviderConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
//#region src/prompts.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Fetches `v1/ai-tasks/templates/message-prompt/:action` (`action` is the task route name, e.g. `generate-commitMessage`) and substitutes
|
|
6
|
+
* the first `${name}` occurrence for each name the template declares in `variables` (a variable
|
|
7
|
+
* with no matching entry in `variables` substitutes the empty string, and an
|
|
8
|
+
* entry with no matching `${name}` placeholder is dropped — both mirror Go's
|
|
9
|
+
* `strings.Replace(..., 1)` loop in `GetPrompt` (`internal/ai/prompts.go`): only the first occurrence of
|
|
10
|
+
* each placeholder is replaced, and the value is inserted literally).
|
|
11
|
+
*/
|
|
12
|
+
declare function getPromptTemplate(config: AiProviderConfig, action: string, variables: Record<string, string>): Promise<string>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { getPromptTemplate };
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AiError as e,toAiError as t,wireErrorFields as n}from"./errors.js";import{HttpError as r,Transport as i}from"@gitkraken/api-tools";const a=`https://api.gitkraken.dev/`;async function o(o,s,c){if(!o.getToken)throw new e(`not_logged_in`,"getPromptTemplate requires `getToken` to be configured");let l=o.provider===`gitkraken`?o.baseUrl??a:o.gitkrakenApiBaseUrl??a,u=new i({baseUrl:l.endsWith(`/`)?l:`${l}/`,getToken:o.getToken,request:o.request}),d;try{let t=(await u.request({method:`GET`,path:`v1/ai-tasks/templates/message-prompt/${s}`})).data;if(typeof t==`object`&&t&&!Array.isArray(t)&&`error`in t&&t.error!=null)throw new e(`provider_error`,`prompt template request returned an error response`,{...n(t)});if(typeof t!=`object`||!t||Array.isArray(t))throw new e(`provider_error`,`prompt template request returned an invalid response`);let r=t.template,i=t.variables;if(typeof r!=`string`||!Array.isArray(i)||!i.every(e=>typeof e==`string`))throw new e(`provider_error`,`prompt template request returned an invalid response`);d={template:r,variables:i}}catch(i){if(i instanceof r){let t={status:i.status,cause:i,...n(i.body)};throw i.status===401?new e(`not_logged_in`,`GitKraken rejected the prompt template request (HTTP 401): the GitKraken token is missing, expired or invalid.`,t):i.status===429?new e(`rate_limited`,`prompt template request was rate limited`,t):i.status>=400&&i.status<500?new e(`invalid_request`,`prompt template request was rejected: ${i.message}`,t):new e(`provider_error`,`failed to fetch the prompt template: ${i.message}`,t)}throw t(i,`failed to fetch the prompt template`)}let f=d.template??``;for(let e of d.variables??[])f=f.replace(`\${${e}}`,()=>c[e]??``);return f}export{o as getPromptTemplate};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { Model, ModelGenerateResult } from "../ports/model.js";
|
|
3
|
+
|
|
4
|
+
//#region src/providers/byok.d.ts
|
|
5
|
+
/** Creates the `Model` port for a BYOK provider (`anthropic`, `openai`, `openrouter`, `ollama`) via the Vercel AI SDK. */
|
|
6
|
+
declare function createByokModel(config: AiProviderConfig): Model;
|
|
7
|
+
/** BYOK task-style single-turn completion, used by `generateCommitMessage` and friends. There is no separate task route for BYOK providers: Go's `Complete` ignores `Kind` for them, so this is the same chat call `generate` makes. */
|
|
8
|
+
declare function byokComplete(config: AiProviderConfig, prompt: string, options?: {
|
|
9
|
+
allowTruncatedOutput?: boolean;
|
|
10
|
+
}): Promise<Omit<ModelGenerateResult, 'toolCalls'>>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { byokComplete, createByokModel };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AiError as e,isAbortError as t,isTruncatedFinishReason as n,truncatedOutputError as r,wireErrorFields as i}from"../errors.js";import{createAnthropic as a}from"@ai-sdk/anthropic";import{createOpenAI as o}from"@ai-sdk/openai";import{createOpenRouter as s}from"@openrouter/ai-sdk-provider";import{generateText as c,jsonSchema as l,stepCountIs as u}from"ai";import{createOllama as d}from"ollama-ai-provider-v2";const f={anthropic:`claude-sonnet-4-5`,openai:`gpt-5`,openrouter:`anthropic/claude-sonnet-4.5`,ollama:`llama3.1`};function p(e){return Object.hasOwn(f,e)}function m(t){let n=t.provider;if(!p(n))throw new e(`invalid_request`,`${n} is not a BYOK provider`);let r=t.fetch??globalThis.fetch,i=t.model||f[n];if(n!==`ollama`&&!t.apiKey)throw new e(`invalid_request`,`the ${n} provider requires an API key`);switch(n){case`anthropic`:return a({apiKey:t.apiKey,baseURL:t.baseUrl,fetch:r})(i);case`openai`:return o({apiKey:t.apiKey,baseURL:t.baseUrl,fetch:r}).chat(i);case`openrouter`:return s({apiKey:t.apiKey,baseURL:t.baseUrl,fetch:r,headers:{"HTTP-Referer":`https://www.gitkraken.com/`,"X-Title":t.client?.name??`GitKraken`}})(i);case`ollama`:return d({baseURL:`${(t.baseUrl??`http://localhost:11434`).replace(/\/$/,``)}/api`,fetch:r})(i)}}function h(e){return e.map(e=>{if(e.role===`tool`)return{role:`tool`,content:[{type:`tool-result`,toolCallId:e.toolCallId,toolName:e.toolName,output:{type:`text`,value:e.content}}]};let t=e.toolCalls??[];return e.role===`assistant`&&t.length>0?{role:`assistant`,content:[...e.content?[{type:`text`,text:e.content}]:[],...t.map(e=>({type:`tool-call`,toolCallId:e.id,toolName:e.name,input:e.args}))]}:{role:e.role,content:e.content??``}})}function g(e){if(!e||e.length===0)return;let t={};for(let n of e)t[n.name]={description:n.description,inputSchema:l(n.parameters)};return t}function _(n){if(n instanceof e||t(n))return n;let r=n instanceof Error?n.message:String(n),a=n?.statusCode,o={cause:n,...a===void 0?{}:{status:a},...i(n?.responseBody)};return a===401||a===403?new e(`not_logged_in`,`AI provider rejected the request: ${r}`,o):a===429?new e(`rate_limited`,`AI request was rate limited: ${r}`,o):a!=null&&a>=400&&a<500?new e(`invalid_request`,r,o):new e(`provider_error`,r,{...o,retryable:!0})}function v(e){let t=m(e);return{async generate(i){let a=i.messages,o=`tools`in i?i.tools:void 0,s=`allowTruncatedOutput`in i?!!i.allowTruncatedOutput:!1,l=i.maxTokens??(e.provider===`anthropic`?8192:void 0);try{let d=await c({model:t,system:i.system,messages:h(a),tools:g(o),stopWhen:u(1),maxRetries:0,maxOutputTokens:l,temperature:i.temperature,abortSignal:i.signal});if(!s&&n(d.finishReason))throw r();return e.telemetry?.event(`ai.generate`,{provider:e.provider}),{text:d.text,finishReason:d.finishReason,...d.rawFinishReason===void 0?{}:{rawFinishReason:d.rawFinishReason},...d.response?.modelId?{model:d.response.modelId}:{},...d.toolCalls.length>0?{toolCalls:d.toolCalls.map(e=>({id:e.toolCallId,name:e.toolName,args:e.input??{}}))}:{},...d.usage&&(d.usage.inputTokens||d.usage.outputTokens)?{usage:{inputTokens:d.usage.inputTokens??0,outputTokens:d.usage.outputTokens??0,...d.usage.inputTokenDetails?.cacheReadTokens?{cacheReadTokens:d.usage.inputTokenDetails.cacheReadTokens}:{},...d.usage.inputTokenDetails?.cacheWriteTokens?{cacheWriteTokens:d.usage.inputTokenDetails.cacheWriteTokens}:{},...d.usage.outputTokenDetails?.reasoningTokens?{reasoningTokens:d.usage.outputTokenDetails.reasoningTokens}:{}}}:{}}}catch(t){let n=_(t);throw e.telemetry?.exception(n,{provider:e.provider}),n}}}}const y=new WeakMap;async function b(e,t,n){let r=y.get(e);r||(r=v(e),y.set(e,r));let{toolCalls:i,...a}=await r.generate({messages:[{role:`user`,content:t}],allowTruncatedOutput:n?.allowTruncatedOutput});return a}export{b as byokComplete,v as createByokModel};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { AiCreditsExhaustedDetails, AiError } from "../errors.js";
|
|
3
|
+
|
|
4
|
+
//#region src/providers/gitkraken-errors.d.ts
|
|
5
|
+
/** Parses `body` as JSON and looks for an explicit credits-exhaustion code anywhere in the tree. */
|
|
6
|
+
declare function creditsExhaustedFromBody(body: string): AiCreditsExhaustedDetails | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Confirms exhaustion from `getCreditUsage` when an otherwise-unclassified
|
|
9
|
+
* quota-shaped response fails. Mirrors Go's `creditsExhaustedError` usage
|
|
10
|
+
* fallback: only 402/403/429 probe, and only `Limit>0 && Used>=Limit` counts
|
|
11
|
+
* as confirmed exhaustion.
|
|
12
|
+
*/
|
|
13
|
+
declare function probeCreditsExhausted(status: number, config: AiProviderConfig): Promise<AiCreditsExhaustedDetails | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* Classifies a non-2xx response from a `gitkraken`-provider request into an
|
|
16
|
+
* `AiError`, checking for credits exhaustion first (explicit body code, then
|
|
17
|
+
* the usage fallback) before falling back to a generic status-based mapping.
|
|
18
|
+
*/
|
|
19
|
+
declare function classifyGitKrakenHttpError(status: number, body: string, config: AiProviderConfig): Promise<AiError>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { classifyGitKrakenHttpError, creditsExhaustedFromBody, probeCreditsExhausted };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AiError as e,wireErrorFields as t}from"../errors.js";const n=new Set([`credits_exhausted`,`credit_exhausted`,`credit_limit_exceeded`,`ai_credits_exhausted`,`insufficient_credits`]);function r(e){return typeof e==`string`&&n.has(e.trim().toLowerCase())}function i(e){if(Array.isArray(e)){for(let t of e){let e=i(t);if(e)return e}return}if(typeof e==`object`&&e){let t=e,n=a(t);if(n)return n;for(let e of Object.values(t)){let t=i(e);if(t)return t}}}function a(e){if(!Object.entries(e).some(([e,t])=>[`code`,`errorcode`,`type`].includes(e.toLowerCase().replaceAll(`_`,``))&&r(t)))return;let t={};for(let[n,r]of Object.entries(e)){let e=n.toLowerCase().replaceAll(`_`,``);e===`used`&&o(r)?t.used=Number(r):e===`limit`&&o(r)?t.limit=Number(r):e===`organizationid`&&typeof r==`string`?t.organizationId=r:e===`resetson`&&typeof r==`string`&&!Number.isNaN(Date.parse(r))&&(t.resetsOn=new Date(r).toISOString())}return t}function o(e){return typeof e==`number`||typeof e==`string`&&e.trim()!==``&&!Number.isNaN(Number(e))}function s(e){try{return i(JSON.parse(e))}catch{return}}const c=new Set([402,403,429]);async function l(e,t){if(!c.has(e)||!t.getCreditUsage)return;let n=new AbortController,r=setTimeout(()=>n.abort(),1e4);try{let e=await t.getCreditUsage(n.signal);return!e||e.limit<=0||e.used<e.limit?void 0:{used:e.used,limit:e.limit,resetsOn:e.resetsOn,organizationId:e.organizationId}}catch{return}finally{clearTimeout(r)}}async function u(n,r,i){let a={status:n,...t(r)},o=s(r);if(o)return new e(`credits_exhausted`,`GitKraken AI credits exhausted`,{...a,details:o});let c=await l(n,i);return c?new e(`credits_exhausted`,`GitKraken AI credits exhausted`,{...a,details:c}):n===401?new e(`not_logged_in`,`GitKraken AI service rejected the request (HTTP 401): the GitKraken token is missing, expired or invalid.`,a):n===429?new e(`rate_limited`,`GitKraken AI service returned HTTP ${n}: ${d(r)}`,a):n>=400&&n<500?new e(`invalid_request`,`GitKraken AI service returned HTTP ${n}: ${d(r)}`,a):new e(`provider_error`,`GitKraken AI service returned HTTP ${n}: ${d(r)}`,{...a,retryable:!0})}function d(e,t=400){return e.length>t?`${e.slice(0,t)}…`:e}export{u as classifyGitKrakenHttpError,s as creditsExhaustedFromBody,l as probeCreditsExhausted};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AiFinishReason } from "../ports/model.js";
|
|
2
|
+
|
|
3
|
+
//#region src/providers/gitkraken-stream.d.ts
|
|
4
|
+
interface AggregatedChatResult {
|
|
5
|
+
model?: string;
|
|
6
|
+
content: string;
|
|
7
|
+
toolCalls: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
args: Record<string, unknown>;
|
|
11
|
+
}>;
|
|
12
|
+
finishReason: string;
|
|
13
|
+
promptTokens: number;
|
|
14
|
+
completionTokens: number;
|
|
15
|
+
totalTokens: number;
|
|
16
|
+
gk?: {
|
|
17
|
+
used?: number;
|
|
18
|
+
limit?: number;
|
|
19
|
+
resetsOn?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
declare function normalizeFinishReason(raw: string): AiFinishReason;
|
|
23
|
+
/**
|
|
24
|
+
* Parses an OpenAI-compatible SSE body (or a non-SSE JSON fallback body) into
|
|
25
|
+
* one aggregated result. A direct port of Go's `parseOpenAICompatibleStream`.
|
|
26
|
+
*/
|
|
27
|
+
declare function parseOpenAICompatibleStream(body: string): AggregatedChatResult;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { AggregatedChatResult, normalizeFinishReason, parseOpenAICompatibleStream };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{AiError as e,isTruncatedFinishReason as t,truncatedOutputError as n}from"../errors.js";import{decodeToolArguments as r}from"./gitkraken-wire.js";const i=new Set([`credits_exhausted`,`credit_exhausted`,`credit_limit_exceeded`,`ai_credits_exhausted`,`insufficient_credits`]);function a(e){switch(e){case`stop`:case`end_turn`:case`stop_sequence`:return`stop`;case`length`:case`max_tokens`:return`length`;case`tool_calls`:case`tool_use`:case`function_call`:return`tool-calls`;case`content_filter`:return`content-filter`;case`error`:return`error`;default:return`other`}}function o(t){let n=(t.code??``).trim().toLowerCase(),r=(t.type??``).trim().toLowerCase(),a={...t.code?{providerErrorCode:t.code}:{},...t.type?{providerErrorType:t.type}:{}};if(i.has(n)||i.has(r))return new e(`credits_exhausted`,`GitKraken AI credits exhausted`,a);let o=t.message||`the AI service stream ended unexpectedly`;return r===`rate_limit_error`?new e(`rate_limited`,`AI request was rate limited: ${o}. Wait a moment and retry.`,a):r===`invalid_request_error`?new e(`invalid_request`,`AI request was rejected: ${o}`,a):new e(`provider_error`,`AI request failed: ${o}. Retry the request.`,a)}function s(t){try{return JSON.parse(t)}catch(t){throw new e(`provider_error`,`AI service returned a malformed response`,{cause:t})}}function c(i){let a=new Map,c=``,m=``,h=``,g=!1,_=!1,v=!1,y,b=0,x=0,S=0,C;for(let e of i.split(`
|
|
2
|
+
`)){let t=e.trim();if(t===``||t.startsWith(`:`))continue;if(!t.startsWith(`data:`)){m+=(m.length>0?`
|
|
3
|
+
`:``)+t;continue}g=!0;let n=t.slice(5).trim();if(n===``)continue;if(n===`[DONE]`){_=!0;break}let r=s(n);if(r.error?.message||r.error?.code||r.error?.type)throw o(r.error);r.model&&(y=r.model),r.usage&&((r.usage.prompt_tokens||r.usage.completion_tokens||r.usage.total_tokens)&&(b=r.usage.prompt_tokens??0,x=r.usage.completion_tokens??0,S=r.usage.total_tokens??0),r.usage.gk?.limit&&(C={used:r.usage.gk.used,limit:r.usage.gk.limit,resetsOn:r.usage.gk.resetsOn}));for(let e of r.choices??[])if((e.index??0)===0){v=!0,e.delta?.content!=null&&(c+=e.delta.content);for(let t of u(e)){let e=a.get(t.index)??{id:``,name:``,args:``};t.id&&(e.id=t.id),e.name+=t.name??``,e.args+=t.args??``,a.set(t.index,e)}e.finish_reason&&(h=e.finish_reason)}}if(!g&&m.length>0){let e=s(m);if(!f(e))throw p();if(e.error!=null){let t=f(e.error)?e.error:{};throw o({...typeof t.message==`string`?{message:t.message}:{},...typeof t.type==`string`?{type:t.type}:{},...typeof t.code==`string`?{code:t.code}:{}})}return l(d(e))}if(!v)throw new e(`provider_error`,`AI service returned no streamed choices`);if(t(h))throw n();if(h===``)throw _?new e(`provider_error`,`AI service returned an incomplete streamed chat completion (the stream ended without a finish reason). Retry the request.`):new e(`provider_error`,`AI service stream ended before the completion marker; response may be incomplete. Retry the request.`);if(c===``&&a.size===0)throw new e(`provider_error`,`AI service returned an empty streamed chat completion. Retry the request.`);let w=[...a.entries()].sort(([e],[t])=>e-t).map(([,e])=>({id:e.id,name:e.name,args:r(e.args)}));return{model:y,content:c,toolCalls:w,finishReason:h,promptTokens:b,completionTokens:x,totalTokens:S,gk:C}}function l(r){if(t(r.finishReason))throw n();if(r.finishReason===``)throw new e(`provider_error`,`AI service returned an incomplete chat completion without a finish reason. Retry the request.`);if(r.content===``&&r.toolCalls.length===0)throw new e(`provider_error`,`AI service returned an empty chat completion. Retry the request.`);return r}function u(e){let t=e.delta?.tool_calls;return t?t.map((e,t)=>({index:e.index??t,id:e.id,name:e.function?.name,args:e.function?.arguments})):[]}function d(e){let t=e.choices?.[0],n=Array.isArray(t?.message?.tool_calls)&&t.message.tool_calls.length>0;if(!t||typeof t.finish_reason!=`string`||t.finish_reason===``||!t.message||t.message.content!==void 0&&t.message.content!==null&&typeof t.message.content!=`string`||t.message.content==null&&!n||t.message.tool_calls!==void 0&&(!Array.isArray(t.message.tool_calls)||t.message.tool_calls.some(e=>!e||typeof e!=`object`||!e.function||typeof e.function.name!=`string`||e.function.arguments!==void 0&&typeof e.function.arguments!=`string`)))throw p();let i=(t?.message?.tool_calls??[]).map(e=>({id:e.id??``,name:e.function?.name??``,args:r(e.function?.arguments)}));return{model:e.model,content:t?.message?.content??``,toolCalls:i,finishReason:t?.finish_reason??``,promptTokens:e.usage?.prompt_tokens??0,completionTokens:e.usage?.completion_tokens??0,totalTokens:e.usage?.total_tokens??0,gk:e.usage?.gk?{used:e.usage.gk.used,limit:e.usage.gk.limit,resetsOn:e.usage.gk.resetsOn}:void 0}}function f(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function p(){return new e(`provider_error`,`AI service returned an invalid non-streamed chat completion`)}export{a as normalizeFinishReason,c as parseOpenAICompatibleStream};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { AiToolDefinition, ToolAwareMessage } from "../ports/model.js";
|
|
2
|
+
|
|
3
|
+
//#region src/providers/gitkraken-wire.d.ts
|
|
4
|
+
/** `v1/ai-tasks/chat/completions` request body. Mirrors Go's `request.ChatCompletionRequest`. */
|
|
5
|
+
interface ChatCompletionWireRequest {
|
|
6
|
+
model?: string;
|
|
7
|
+
messages: ChatCompletionWireMessage[];
|
|
8
|
+
max_tokens?: number;
|
|
9
|
+
stream: true;
|
|
10
|
+
stream_options: {
|
|
11
|
+
include_usage: true;
|
|
12
|
+
};
|
|
13
|
+
temperature: number;
|
|
14
|
+
tools?: ChatCompletionWireTool[];
|
|
15
|
+
}
|
|
16
|
+
interface ChatCompletionWireMessage {
|
|
17
|
+
role: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
tool_call_id?: string;
|
|
20
|
+
tool_calls?: ChatCompletionWireToolCall[];
|
|
21
|
+
}
|
|
22
|
+
interface ChatCompletionWireTool {
|
|
23
|
+
type: 'function';
|
|
24
|
+
function: {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
parameters?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
interface ChatCompletionWireToolCall {
|
|
31
|
+
id?: string;
|
|
32
|
+
type?: 'function';
|
|
33
|
+
function: {
|
|
34
|
+
name?: string;
|
|
35
|
+
arguments?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
interface GkCreditUsageWire {
|
|
39
|
+
resetsOn?: string;
|
|
40
|
+
used?: number;
|
|
41
|
+
limit?: number;
|
|
42
|
+
}
|
|
43
|
+
interface ChatCompletionWireResponse {
|
|
44
|
+
error?: {
|
|
45
|
+
message?: string;
|
|
46
|
+
type?: string;
|
|
47
|
+
code?: string;
|
|
48
|
+
};
|
|
49
|
+
id?: string;
|
|
50
|
+
object?: string;
|
|
51
|
+
created?: number;
|
|
52
|
+
model?: string;
|
|
53
|
+
choices?: Array<{
|
|
54
|
+
index?: number;
|
|
55
|
+
message?: {
|
|
56
|
+
role?: string;
|
|
57
|
+
content?: string | null;
|
|
58
|
+
tool_calls?: ChatCompletionWireToolCall[];
|
|
59
|
+
};
|
|
60
|
+
finish_reason?: string;
|
|
61
|
+
}>;
|
|
62
|
+
usage?: {
|
|
63
|
+
model?: string;
|
|
64
|
+
gk?: GkCreditUsageWire;
|
|
65
|
+
prompt_tokens?: number;
|
|
66
|
+
completion_tokens?: number;
|
|
67
|
+
total_tokens?: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** `v1/ai-tasks` (task route) request body. Mirrors Go's `request.ChatAITasksRequest`. */
|
|
71
|
+
interface TaskWireRequest {
|
|
72
|
+
taskType: 'message-prompt';
|
|
73
|
+
action: string;
|
|
74
|
+
messages: [{
|
|
75
|
+
content: string;
|
|
76
|
+
}];
|
|
77
|
+
model?: string;
|
|
78
|
+
}
|
|
79
|
+
interface TaskWireResponse {
|
|
80
|
+
error?: {
|
|
81
|
+
message?: string;
|
|
82
|
+
code?: string;
|
|
83
|
+
};
|
|
84
|
+
data?: {
|
|
85
|
+
id?: string;
|
|
86
|
+
status?: string;
|
|
87
|
+
provider?: string;
|
|
88
|
+
result?: {
|
|
89
|
+
content?: string;
|
|
90
|
+
role?: string;
|
|
91
|
+
};
|
|
92
|
+
usage?: {
|
|
93
|
+
gk?: GkCreditUsageWire;
|
|
94
|
+
modelId?: string;
|
|
95
|
+
promptTokens?: number;
|
|
96
|
+
completionTokens?: number;
|
|
97
|
+
totalTokens?: number;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** One `data:` line of the SSE stream. Mirrors Go's `response.ChatCompletionStreamResponse`. */
|
|
102
|
+
interface ChatCompletionStreamChunk {
|
|
103
|
+
error?: {
|
|
104
|
+
message?: string;
|
|
105
|
+
type?: string;
|
|
106
|
+
code?: string;
|
|
107
|
+
};
|
|
108
|
+
id?: string;
|
|
109
|
+
object?: string;
|
|
110
|
+
created?: number;
|
|
111
|
+
model?: string;
|
|
112
|
+
choices?: Array<{
|
|
113
|
+
index?: number;
|
|
114
|
+
delta?: {
|
|
115
|
+
role?: string;
|
|
116
|
+
content?: string | null;
|
|
117
|
+
tool_calls?: Array<ChatCompletionWireToolCall & {
|
|
118
|
+
index?: number;
|
|
119
|
+
}>;
|
|
120
|
+
};
|
|
121
|
+
finish_reason?: string;
|
|
122
|
+
}>;
|
|
123
|
+
usage?: {
|
|
124
|
+
model?: string;
|
|
125
|
+
gk?: GkCreditUsageWire;
|
|
126
|
+
prompt_tokens?: number;
|
|
127
|
+
completion_tokens?: number;
|
|
128
|
+
total_tokens?: number;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
interface ModelsWireResponse {
|
|
132
|
+
data?: Array<{
|
|
133
|
+
providerId?: string;
|
|
134
|
+
providerName?: string;
|
|
135
|
+
modelId?: string;
|
|
136
|
+
modelName?: string;
|
|
137
|
+
maxInputTokens?: number;
|
|
138
|
+
maxOutputTokens?: number;
|
|
139
|
+
preferred?: boolean;
|
|
140
|
+
consumptionRateLabel?: string;
|
|
141
|
+
}>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Renders one wire message's `content`, omitting it (rather than sending
|
|
145
|
+
* `""`) for an assistant turn that carries only tool calls. Mirrors Go's
|
|
146
|
+
* `providers.WireContent`: some gateways translate `"content": ""` into an
|
|
147
|
+
* empty text block that the upstream rejects.
|
|
148
|
+
*/
|
|
149
|
+
declare function wireContent(content: string | undefined, hasToolCalls: boolean): string | undefined;
|
|
150
|
+
declare function encodeToolArguments(args: Record<string, unknown> | undefined): string;
|
|
151
|
+
declare function decodeToolArguments(raw: string | undefined): Record<string, unknown>;
|
|
152
|
+
/** Maps port-level messages to the GitKraken chat/completions wire shape. */
|
|
153
|
+
declare function toWireMessages(messages: ToolAwareMessage[]): ChatCompletionWireMessage[];
|
|
154
|
+
declare function toWireTools(tools: AiToolDefinition[] | undefined): ChatCompletionWireTool[] | undefined;
|
|
155
|
+
//#endregion
|
|
156
|
+
export { ChatCompletionStreamChunk, ChatCompletionWireMessage, ChatCompletionWireRequest, ChatCompletionWireResponse, ChatCompletionWireTool, ChatCompletionWireToolCall, GkCreditUsageWire, ModelsWireResponse, TaskWireRequest, TaskWireResponse, decodeToolArguments, encodeToolArguments, toWireMessages, toWireTools, wireContent };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e,t){if(!((e??``)===``&&t))return e??``}function t(e){return!e||Object.keys(e).length===0?`{}`:JSON.stringify(e)}function n(e){if(!e)return{};let t=JSON.parse(e);return typeof t!=`object`||!t?{}:t}function r(n){return n.map(n=>{if(n.role===`tool`)return{role:`tool`,content:n.content,tool_call_id:n.toolCallId};let r=n.role===`assistant`?n.toolCalls??[]:[],i={role:n.role,content:e(n.content,r.length>0)};return r.length>0&&(i.tool_calls=r.map(e=>({id:e.id,type:`function`,function:{name:e.name,arguments:t(e.args)}}))),i})}function i(e){if(!(!e||e.length===0))return e.map(e=>({type:`function`,function:{name:e.name,description:e.description,parameters:e.parameters}}))}export{n as decodeToolArguments,t as encodeToolArguments,r as toWireMessages,i as toWireTools,e as wireContent};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AiProviderConfig } from "../config.js";
|
|
2
|
+
import { AiToolDefinition, Model, ModelGenerateResult, ToolAwareMessage } from "../ports/model.js";
|
|
3
|
+
import { AiModel } from "../models.js";
|
|
4
|
+
|
|
5
|
+
//#region src/providers/gitkraken.d.ts
|
|
6
|
+
/** The hosted task record behind a `gitkraken` task-route completion, with raw API values. */
|
|
7
|
+
interface AiTaskInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
/** e.g. `completed`. */
|
|
10
|
+
status?: string;
|
|
11
|
+
/** The backend that served the task, e.g. `gemini`. */
|
|
12
|
+
provider?: string;
|
|
13
|
+
}
|
|
14
|
+
interface ChatCompleteRequest {
|
|
15
|
+
/** Sent as the `GK-Action` header. Defaults to `'compose-changes'`. */
|
|
16
|
+
action?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
/** Sent as the `GK-Conversation-ID` header. */
|
|
19
|
+
conversationId?: string;
|
|
20
|
+
system?: string;
|
|
21
|
+
messages: ToolAwareMessage[];
|
|
22
|
+
tools?: AiToolDefinition[];
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
temperature?: number;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}
|
|
27
|
+
/** POSTs `v1/ai-tasks/chat/completions` (streamed) and aggregates the SSE response. */
|
|
28
|
+
declare function chatComplete(config: AiProviderConfig, request: ChatCompleteRequest): Promise<ModelGenerateResult>;
|
|
29
|
+
/** Creates the `Model` port for `provider: 'gitkraken'`. `generate` always uses the chat/completions route (KindChat in Go). */
|
|
30
|
+
declare function createGitKrakenModel(config: AiProviderConfig): Model;
|
|
31
|
+
/**
|
|
32
|
+
* POSTs `v1/ai-tasks` (the non-streamed task route) with a single rendered
|
|
33
|
+
* prompt and returns the raw completion content and the token usage the route
|
|
34
|
+
* reports. Used by `generateCommitMessage` and friends, which parse the
|
|
35
|
+
* XML-wrapped content themselves.
|
|
36
|
+
*/
|
|
37
|
+
declare function taskComplete(config: AiProviderConfig, options: {
|
|
38
|
+
action: string;
|
|
39
|
+
prompt: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<Pick<ModelGenerateResult, 'text' | 'usage' | 'model'> & {
|
|
43
|
+
task?: AiTaskInfo;
|
|
44
|
+
}>;
|
|
45
|
+
/** GETs `v1/ai-tasks/providers/:taskType`. Defaults `taskType` to `message-prompt`. */
|
|
46
|
+
declare function listModels(config: AiProviderConfig, taskType?: string): Promise<AiModel[]>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { AiTaskInfo, ChatCompleteRequest, chatComplete, createGitKrakenModel, listModels, taskComplete };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AiError as e,isAbortError as t,toAiError as n,wireErrorFields as r}from"../errors.js";import{classifyGitKrakenHttpError as i}from"./gitkraken-errors.js";import{toWireMessages as a,toWireTools as o,wireContent as s}from"./gitkraken-wire.js";import{normalizeFinishReason as c,parseOpenAICompatibleStream as l}from"./gitkraken-stream.js";import{defaultRequest as u}from"@gitkraken/api-tools";const d=`message-prompt`;function f(e,t=``){let n=e.baseUrl??`https://api.gitkraken.dev/`,r=n.endsWith(`/`)?n:`${n}/`;return new URL(`v1/ai-tasks${t}`,r).toString()}async function p(n){if(!n.getToken)throw new e(`not_logged_in`,"the gitkraken provider requires `getToken` to be configured");try{return await n.getToken()}catch(n){throw t(n)?n:new e(`not_logged_in`,`failed to obtain a GitKraken token`,{cause:n})}}async function m(e,t){let n={Authorization:`Bearer ${await p(e)}`};return e.organizationId&&(n[`GK-Org-Id`]=e.organizationId),t&&(n[`GK-Action`]=t),e.client&&(n[`Client-Name`]=e.client.name,n[`Client-Version`]=e.client.version),n}function h(e){return typeof e==`string`?e:e==null?``:JSON.stringify(e)}function g(t){if(typeof t==`string`){if(t.length===0)return{};try{return JSON.parse(t)}catch(t){throw new e(`provider_error`,`AI service returned a malformed response`,{cause:t})}}return t??{}}function _(e,t){!t||!e.onCreditUsage||t.limit==null||t.limit<=0||t.used==null||e.onCreditUsage({used:t.used,limit:t.limit,resetsOn:t.resetsOn&&!Number.isNaN(Date.parse(t.resetsOn))?new Date(t.resetsOn).toISOString():t.resetsOn,organizationId:e.organizationId})}async function v(e,t){try{let n=a(t.messages);t.system&&n.unshift({role:`system`,content:s(t.system,!1)});let r={model:t.model||void 0,messages:n,max_tokens:t.maxTokens||void 0,stream:!0,stream_options:{include_usage:!0},temperature:t.temperature??0,tools:o(t.tools)},u=await m(e,t.action||`compose-changes`);u[`Content-Type`]=`application/json`,u.Accept=`text/event-stream`,t.conversationId&&(u[`GK-Conversation-ID`]=t.conversationId);let d=await(e.fetch??globalThis.fetch)(f(e,`/chat/completions`),{method:`POST`,redirect:`manual`,headers:u,body:JSON.stringify(r),signal:t.signal}),p=await d.text();if(!d.ok)throw await i(d.status,p,e);let h=l(p);return _(e,h.gk),e.telemetry?.event(`ai.generate`,{provider:`gitkraken`,kind:`chat`}),{text:h.content,toolCalls:h.toolCalls.length>0?h.toolCalls:void 0,usage:h.promptTokens||h.completionTokens||h.totalTokens?{inputTokens:h.promptTokens,outputTokens:h.completionTokens}:void 0,finishReason:c(h.finishReason),rawFinishReason:h.finishReason,...h.model?{model:h.model}:{}}}catch(t){throw e.telemetry?.exception(t,{provider:`gitkraken`,kind:`chat`}),n(t,`AI request failed`)}}function y(e){return{async generate(t){let n=t.messages,r=`tools`in t?t.tools:void 0;return v(e,{model:e.model,system:t.system,messages:n,tools:r,maxTokens:t.maxTokens,temperature:t.temperature,signal:t.signal})}}}async function b(t,a){try{let n=t.request??u,o={taskType:d,action:a.action,messages:[{content:a.prompt}],model:(a.model??t.model)||void 0},s=await n({method:`POST`,url:f(t),headers:{...await m(t,a.action),"Content-Type":`application/json`},body:o,signal:a.signal});if(!(s.status>=200&&s.status<300))throw await i(s.status,h(s.body),t);let c=g(s.body);if(!S(c))throw C(`task`);let l=c.error;if(l!=null){let t=r(c);throw new e(`provider_error`,`AI request failed: ${S(l)&&typeof l.message==`string`?l.message:`unknown provider error`}`,t)}let p=c.data;if(!S(p))throw C(`task`);if(p.status===`error`||p.status===`failed`)throw new e(`provider_error`,`AI task failed: ${String(p.status)}`,{...r(p),...typeof p.code==`string`?{providerErrorCode:p.code}:{},...typeof p.type==`string`?{providerErrorType:p.type}:{}});if(!S(p.result)||typeof p.result.content!=`string`)throw C(`task`);let v=p.result.content,y=p,b=y.usage;return _(t,b?.gk),t.telemetry?.event(`ai.generate`,{provider:`gitkraken`,kind:`task`}),{text:v,...b?.modelId?{model:b.modelId}:{},...y.id?{task:{id:y.id,...y.status?{status:y.status}:{},...y.provider?{provider:y.provider}:{}}}:{},...b?.promptTokens||b?.completionTokens?{usage:{inputTokens:b.promptTokens??0,outputTokens:b.completionTokens??0}}:{}}}catch(e){throw t.telemetry?.exception(e,{provider:`gitkraken`,kind:`task`}),n(e,`AI request failed`)}}async function x(t,a){let o=t.request??u,s;try{let e=await o({method:`GET`,url:f(t,`/providers/${a||d}`),headers:await m(t,void 0)});if(!(e.status>=200&&e.status<300))throw await i(e.status,h(e.body),t);s=g(e.body)}catch(e){throw n(e,`failed to list AI models`)}if(!S(s))throw C(`model list`);if(s.error!=null)throw new e(`provider_error`,`AI model list request returned an error response`,r(s));if(!Array.isArray(s.data))throw C(`model list`);return s.data.map(e=>{if(!S(e)||typeof e.providerId!=`string`||!e.providerId||typeof e.modelId!=`string`||!e.modelId||e.providerName!==void 0&&typeof e.providerName!=`string`||e.modelName!==void 0&&typeof e.modelName!=`string`||e.consumptionRateLabel!==void 0&&typeof e.consumptionRateLabel!=`string`||e.maxInputTokens!==void 0&&typeof e.maxInputTokens!=`number`||e.maxOutputTokens!==void 0&&typeof e.maxOutputTokens!=`number`||e.preferred!==void 0&&typeof e.preferred!=`boolean`)throw C(`model list`);let t=e;return{providerId:t.providerId,providerName:t.providerName,modelId:t.modelId,modelName:t.modelName,maxInputTokens:t.maxInputTokens,maxOutputTokens:t.maxOutputTokens,preferred:t.preferred??!1,consumptionRateLabel:t.consumptionRateLabel}})}function S(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function C(t){return new e(`provider_error`,`AI service returned an invalid ${t} response`)}export{v as chatComplete,y as createGitKrakenModel,x as listModels,b as taskComplete};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AiProviderConfig } from "./config.js";
|
|
2
|
+
import { ModelGenerateResult } from "./ports/model.js";
|
|
3
|
+
import { AiTaskInfo } from "./providers/gitkraken.js";
|
|
4
|
+
|
|
5
|
+
//#region src/run-prompt-completion.d.ts
|
|
6
|
+
interface RunPromptCompletionOptions {
|
|
7
|
+
/** Return a clipped completion instead of throwing `truncated_output`. Only honoured by BYOK providers; defaults to `false`. */
|
|
8
|
+
allowTruncatedOutput?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The completion text plus whatever the provider reported alongside it. The
|
|
12
|
+
* hosted task route reports the model id, token usage and its task record;
|
|
13
|
+
* BYOK providers report the finish reason, model id and token usage.
|
|
14
|
+
*/
|
|
15
|
+
type PromptCompletionResult = Pick<ModelGenerateResult, 'text' | 'usage' | 'finishReason' | 'rawFinishReason' | 'model'> & {
|
|
16
|
+
/** `gitkraken` only. */task?: AiTaskInfo;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Runs a single-turn task prompt: fetches and substitutes the server-side
|
|
20
|
+
* template for `action` (the `GK-Action`/template route name, e.g.
|
|
21
|
+
* `generate-commitMessage`), then completes it. For `gitkraken` this hits the
|
|
22
|
+
* non-streamed task route (`v1/ai-tasks`, `GK-Action` header), whose
|
|
23
|
+
* response carries no finish reason, so truncation is undetectable there. For
|
|
24
|
+
* a BYOK provider there is no separate task route, so it is the same
|
|
25
|
+
* single-message call `generate` makes, and `allowTruncatedOutput` applies
|
|
26
|
+
* exactly as it does there. Ported from gkcli's `aiService.runPromptCompletion`.
|
|
27
|
+
*/
|
|
28
|
+
declare function runPromptCompletion(config: AiProviderConfig, action: string, variables: Record<string, string>, options?: RunPromptCompletionOptions): Promise<PromptCompletionResult>;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { PromptCompletionResult, RunPromptCompletionOptions, runPromptCompletion };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{byokComplete as e}from"./providers/byok.js";import{taskComplete as t}from"./providers/gitkraken.js";import{getPromptTemplate as n}from"./prompts.js";async function r(r,i,a,o){let s=await n(r,i,a);return r.provider===`gitkraken`?t(r,{action:i,prompt:s}):e(r,s,{allowTruncatedOutput:o?.allowTruncatedOutput??!1})}export{r as runPromptCompletion};
|
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/xml.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A direct port of Go's `toJSON` + `response.XMLResultContent`: reads the
|
|
4
|
+
* `<summary>`/`<body>` elements out of a prompt completion that is XML
|
|
5
|
+
* fragments, not a full document (Go wraps it in a synthetic `<Content>`
|
|
6
|
+
* root before unmarshalling).
|
|
7
|
+
*/
|
|
8
|
+
interface XmlResultContent {
|
|
9
|
+
summary: string;
|
|
10
|
+
body: string;
|
|
11
|
+
}
|
|
12
|
+
declare function parseXmlResult(content: string): XmlResultContent;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { XmlResultContent, parseXmlResult };
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={lt:`<`,gt:`>`,amp:`&`,quot:`"`,apos:`'`};function t(t){return t.replace(/&(lt|gt|amp|quot|apos|#\d+|#x[0-9a-fA-F]+);/g,(t,r)=>r in e?e[r]:r.startsWith(`#x`)?n(Number.parseInt(r.slice(2),16),t):r.startsWith(`#`)?n(Number.parseInt(r.slice(1),10),t):t)}function n(e,t){return e<=1114111?String.fromCodePoint(e):t}function r(e,n){let r=e.match(/<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<[^>]*>|[^<]+|</g)??[],i=RegExp(`^<\\/?${n}(?:\\s[^>]*)?\\s*/?>$`),a=0,o=!1,s=``;for(let e of r)if(!e.startsWith(`<!--`)){if(e.startsWith(`<![CDATA[`)){o&&(s+=e.slice(9,-3));continue}if(e.startsWith(`<`)){if(!i.test(e)){o&&a>0&&(s+=e);continue}let t=e.startsWith(`</`),n=e.endsWith(`/>`);if(!o){!t&&!n&&(o=!0,a=1);continue}if(t){if(--a,a===0)return s;s+=e}else n||(a+=1),s+=e;continue}o&&a>0&&(s+=t(e))}return``}function i(e){return{summary:r(e,`summary`),body:r(e,`body`)}}export{i as parseXmlResult};
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/ai-provider-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "AI text generation for GitKraken client SDKs: GitKraken-hosted and BYOK (Anthropic, OpenAI, OpenRouter, Ollama) providers behind one model port.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"author": "Axosoft, LLC dba GitKraken",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">= 22"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./generators": {
|
|
18
|
+
"types": "./dist/generators/index.d.ts",
|
|
19
|
+
"default": "./dist/generators/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"CHANGELOG.md"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@ai-sdk/anthropic": "^3.0.64",
|
|
28
|
+
"@ai-sdk/openai": "^3.0.48",
|
|
29
|
+
"@openrouter/ai-sdk-provider": "^2.3.3",
|
|
30
|
+
"ai": "^6.0.138",
|
|
31
|
+
"ollama-ai-provider-v2": "^3.5.0",
|
|
32
|
+
"@gitkraken/api-tools": "^0.3.0",
|
|
33
|
+
"@gitkraken/shared-tools": "^0.3.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^6.0.2",
|
|
38
|
+
"vitest": "^4.1.1",
|
|
39
|
+
"@gitkraken/conflict-tools": "0.6.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown --config-loader=unrun",
|
|
43
|
+
"build:dev": "tsdown --config tsdown.dev.config.ts --config-loader=unrun",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
}
|
|
6
47
|
}
|