lemura 1.0.0
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 +34 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/adapters/index.d.mts +45 -0
- package/dist/adapters/index.d.ts +45 -0
- package/dist/adapters/index.js +371 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/index.mjs +369 -0
- package/dist/adapters/index.mjs.map +1 -0
- package/dist/adapters-BSkhv5ac.d.ts +208 -0
- package/dist/adapters-BnG0LEYD.d.mts +208 -0
- package/dist/context/index.d.mts +143 -0
- package/dist/context/index.d.ts +143 -0
- package/dist/context/index.js +321 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/index.mjs +314 -0
- package/dist/context/index.mjs.map +1 -0
- package/dist/index.d.mts +91 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +1375 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1348 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger/index.d.mts +19 -0
- package/dist/logger/index.d.ts +19 -0
- package/dist/logger/index.js +67 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/index.mjs +65 -0
- package/dist/logger/index.mjs.map +1 -0
- package/dist/logger-DxvKliuk.d.mts +37 -0
- package/dist/logger-DxvKliuk.d.ts +37 -0
- package/dist/rag/index.d.mts +10 -0
- package/dist/rag/index.d.ts +10 -0
- package/dist/rag/index.js +43 -0
- package/dist/rag/index.js.map +1 -0
- package/dist/rag/index.mjs +41 -0
- package/dist/rag/index.mjs.map +1 -0
- package/dist/rag-La_Bo-J8.d.mts +45 -0
- package/dist/rag-La_Bo-J8.d.ts +45 -0
- package/dist/skills/index.d.mts +15 -0
- package/dist/skills/index.d.ts +15 -0
- package/dist/skills/index.js +40 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/index.mjs +38 -0
- package/dist/skills/index.mjs.map +1 -0
- package/dist/skills-wc8S-OvC.d.mts +14 -0
- package/dist/skills-wc8S-OvC.d.ts +14 -0
- package/dist/storage-BGu4Loao.d.ts +121 -0
- package/dist/storage-DMcliVVj.d.mts +121 -0
- package/dist/tools/index.d.mts +17 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.js +72 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/index.mjs +70 -0
- package/dist/tools/index.mjs.map +1 -0
- package/dist/types/index.d.mts +118 -0
- package/dist/types/index.d.ts +118 -0
- package/dist/types/index.js +84 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +74 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +79 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-03-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Structured logging system with colors and severity levels (FATAL, ERROR, WARN, INFO, DEBUG).
|
|
12
|
+
- Integrated logging into `SessionManager` and `OpenAICompatibleAdapter`.
|
|
13
|
+
- Added `problem` and `hints` to `LemuraError` for better end-user feedback.
|
|
14
|
+
- Dedicated `ILogger` interface and `DefaultLogger` implementation.
|
|
15
|
+
- Short Term Memory (STM) system for persistent memory across session boundaries
|
|
16
|
+
- Scratchpad tools for managing agent's internal reasoning state
|
|
17
|
+
- `ShortTermMemoryRegistry` for memory item lifecycle management
|
|
18
|
+
- `summarize_sandwich` tool for context compression using sandwich strategy
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `SessionManager` — Core ReAct runtime integration entry point, now integrated with logging.
|
|
22
|
+
- `OpenAICompatibleAdapter` — Reference adapter for OpenAI, now integrated with logging.
|
|
23
|
+
|
|
24
|
+
### Added (Core)
|
|
25
|
+
- `OpenAICompatibleAdapter` — Reference adapter for OpenAI and standard API-compatible providers.
|
|
26
|
+
- `ContextManager` — Core context logic coordinating multiple string reduction behaviors.
|
|
27
|
+
- `SandwichCompressionStrategy` — Strategy for preserving recency and foundation context.
|
|
28
|
+
- `HistoryCompressionStrategy` — Strategy for compressing history via summarization.
|
|
29
|
+
- `SessionManager` — Core ReAct runtime integration entry point.
|
|
30
|
+
- `ToolRegistry` — Standardized tooling implementation.
|
|
31
|
+
- `SkillInjector` — Advanced dynamic system prompting mechanism.
|
|
32
|
+
- `ToolResponseProcessor` — Evaluates and handles heavy responses.
|
|
33
|
+
- `ContinuationPlanner` — Multi-step sequential tool handling abstraction.
|
|
34
|
+
- `InMemoryRAGAdapter` — Minimal in-memory document ingestion/query layer.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rzafiamy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<img src="./docs/assets/logo.png" alt="lemura logo" width="200" />
|
|
2
|
+
|
|
3
|
+
# lemura
|
|
4
|
+
|
|
5
|
+
**A provider-agnostic, premium agentic AI runtime for the modern web.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/lemura)
|
|
8
|
+
[](https://github.com/rzafiamy/lemura/blob/main/LICENSE)
|
|
9
|
+
[](https://github.com/rzafiamy/lemura/actions)
|
|
10
|
+
[](https://codecov.io/gh/lemura-ai/lemura)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
`lemura` is a robust, provider-agnostic npm package designed to encapsulate a full agentic AI runtime. It simplifies the complex orchestration of LLMs, tools, and context management into a single, cohesive interface.
|
|
15
|
+
|
|
16
|
+
## 🚀 Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add lemura
|
|
20
|
+
# or
|
|
21
|
+
npm install lemura
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## ⚙️ Environment Variables
|
|
25
|
+
|
|
26
|
+
The built-in `OpenAICompatibleAdapter` can be configured using environment variables. To load them from a `.env` file in Node.js, you'll typically need a library like `dotenv`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install dotenv
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then at the very top of your entry point:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import 'dotenv/config';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Create a `.env` file in your project root:
|
|
39
|
+
|
|
40
|
+
```ini
|
|
41
|
+
# Provider Configuration (OpenAI, Groq, Together, Ollama, etc.)
|
|
42
|
+
LEMURA_API_KEY=your_api_key_here
|
|
43
|
+
LEMURA_BASE_URL=https://api.openai.com/v1
|
|
44
|
+
LEMURA_MODEL=gpt-4o-mini
|
|
45
|
+
|
|
46
|
+
# Fallbacks (Lemura also checks standard OpenAI variables)
|
|
47
|
+
OPENAI_API_KEY=your_api_key_here
|
|
48
|
+
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
49
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## ⚡ Quick Start
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { SessionManager, OpenAICompatibleAdapter } from 'lemura';
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
const adapter = new OpenAICompatibleAdapter({
|
|
59
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
60
|
+
apiKey: process.env.OPENAI_API_KEY || '',
|
|
61
|
+
defaultModel: 'gpt-4o-mini'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const session = new SessionManager({
|
|
65
|
+
adapter,
|
|
66
|
+
model: 'gpt-4o-mini',
|
|
67
|
+
maxTokens: 100000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const response = await session.run('What is lemura?');
|
|
71
|
+
console.log(response);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 🧠 Core Concepts
|
|
78
|
+
|
|
79
|
+
Explore the architecture and advanced capabilities of `lemura`:
|
|
80
|
+
|
|
81
|
+
- 🏁 [**Getting Started**](./docs/guides/getting-started.md) — Fundamental setup and concepts.
|
|
82
|
+
- 🧹 [**Context Management**](./docs/guides/context-management.md) — Advanced compression strategies.
|
|
83
|
+
- 🔌 [**Adapters**](./docs/guides/adapters.md) — Connecting to OpenAI, Groq, Anthropic, and more.
|
|
84
|
+
- 🛠️ [**Tools and Skills**](./docs/guides/tools-and-skills.md) — Extending agent capabilities.
|
|
85
|
+
- ⚡ [**Advanced Execution**](./docs/guides/advanced-execution.md) — Goal planning and continuation.
|
|
86
|
+
|
|
87
|
+
## 📦 API Overview
|
|
88
|
+
|
|
89
|
+
| Export | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `SessionManager` | The main entry point orchestrating the ReAct loop and tools. |
|
|
92
|
+
| `ContextManager` | Manages the conversation history using compression strategies. |
|
|
93
|
+
| `OpenAICompatibleAdapter` | Reference adapter for OpenAI, Groq, Together, etc. |
|
|
94
|
+
| `ToolRegistry` | Registers and executes tools for the agent. |
|
|
95
|
+
| `SkillInjector` | Loads and formats YAML/Markdown skills into system prompts. |
|
|
96
|
+
| `DefaultLogger` | Colorized logger with Problem/Hints metadata support. |
|
|
97
|
+
|
|
98
|
+
## 🪵 Logging and Tracing
|
|
99
|
+
|
|
100
|
+
`lemura` features a premium, structured logging system designed for developer experience. It provides colorized output and actionable hints for errors.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { SessionManager, DefaultLogger, LogLevel } from 'lemura';
|
|
104
|
+
|
|
105
|
+
const logger = new DefaultLogger();
|
|
106
|
+
logger.setLevel(LogLevel.DEBUG); // Set to show trace-level information
|
|
107
|
+
|
|
108
|
+
const session = new SessionManager({
|
|
109
|
+
adapter,
|
|
110
|
+
model: 'gpt-4o-mini',
|
|
111
|
+
maxTokens: 100000,
|
|
112
|
+
logger: logger // Inject the logger
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
When an error occurs (like an invalid API key), `lemura` provides beautiful, structured feedback:
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
2026-03-07T13:05:49.686Z [FATAL] Provider call failed: HTTP 401: Unauthorized
|
|
120
|
+
PROBLEM: Authentication failed. The API key is invalid or missing.
|
|
121
|
+
HINTS:
|
|
122
|
+
- Ensure your API key is correctly configured in the adapter or environment variables.
|
|
123
|
+
- Check if the API key has expired or been revoked.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 🔌 Provider Adapters
|
|
127
|
+
|
|
128
|
+
`lemura` interacts with LLMs exclusively through the `IProviderAdapter` interface, ensuring zero lock-in.
|
|
129
|
+
|
|
130
|
+
| Adapter | Status | Description |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `OpenAICompatibleAdapter` | ✅ Built-in | Wrapper for OpenAI and API-compatible endpoints. |
|
|
133
|
+
|
|
134
|
+
> [!TIP]
|
|
135
|
+
> To write a custom adapter for another provider, see the [Custom Adapter Recipe](./docs/recipes/custom-adapter.md).
|
|
136
|
+
|
|
137
|
+
## 🤝 Contributing
|
|
138
|
+
|
|
139
|
+
We welcome contributions! Please read our [Internal Rules](./.cursor/rules/Project.md) and [Documentation Guidelines](./.cursor/rules/Documentation.md) before submitting a PR.
|
|
140
|
+
|
|
141
|
+
## 📄 License
|
|
142
|
+
|
|
143
|
+
Distributed under the **MIT License**. See `LICENSE` for more information.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { e as IProviderAdapter, c as CompletionRequest, d as CompletionResponse, b as CompletionChunk, M as ModelInfo, k as TranscriptionRequest, l as TranscriptionResponse, S as SynthesisRequest, A as AudioChunk, V as VisionRequest, m as VisionResponse, f as ImageGenRequest, g as ImageGenResponse } from '../adapters-BnG0LEYD.mjs';
|
|
2
|
+
import '../storage-DMcliVVj.mjs';
|
|
3
|
+
import '../rag-La_Bo-J8.mjs';
|
|
4
|
+
import '../logger-DxvKliuk.mjs';
|
|
5
|
+
|
|
6
|
+
interface RetryConfig {
|
|
7
|
+
maxRetries: number;
|
|
8
|
+
baseDelayMs: number;
|
|
9
|
+
}
|
|
10
|
+
interface OpenAICompatibleAdapterConfig {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
defaultModel?: string;
|
|
14
|
+
defaultHeaders?: Record<string, string>;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
retry?: RetryConfig;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reference implementation of an OpenAI-compatible provider adapter.
|
|
20
|
+
*/
|
|
21
|
+
declare class OpenAICompatibleAdapter implements IProviderAdapter {
|
|
22
|
+
readonly name = "openai_compatible";
|
|
23
|
+
readonly version = "1.0.0";
|
|
24
|
+
private baseUrl;
|
|
25
|
+
private apiKey;
|
|
26
|
+
private defaultModel;
|
|
27
|
+
private defaultHeaders;
|
|
28
|
+
private timeoutMs;
|
|
29
|
+
private retryConfig;
|
|
30
|
+
constructor(config?: OpenAICompatibleAdapterConfig);
|
|
31
|
+
private fetchWithRetry;
|
|
32
|
+
private mapFinishReason;
|
|
33
|
+
private buildPayload;
|
|
34
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
35
|
+
stream(request: CompletionRequest): AsyncIterable<CompletionChunk>;
|
|
36
|
+
estimateTokens(text: string): number;
|
|
37
|
+
getModelInfo(): ModelInfo;
|
|
38
|
+
healthCheck(): Promise<boolean>;
|
|
39
|
+
transcribe(request: TranscriptionRequest): Promise<TranscriptionResponse>;
|
|
40
|
+
synthesize(request: SynthesisRequest): AsyncIterable<AudioChunk>;
|
|
41
|
+
describeImage(request: VisionRequest): Promise<VisionResponse>;
|
|
42
|
+
generateImage(request: ImageGenRequest): Promise<ImageGenResponse>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { OpenAICompatibleAdapter, type OpenAICompatibleAdapterConfig, type RetryConfig };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { e as IProviderAdapter, c as CompletionRequest, d as CompletionResponse, b as CompletionChunk, M as ModelInfo, k as TranscriptionRequest, l as TranscriptionResponse, S as SynthesisRequest, A as AudioChunk, V as VisionRequest, m as VisionResponse, f as ImageGenRequest, g as ImageGenResponse } from '../adapters-BSkhv5ac.js';
|
|
2
|
+
import '../storage-BGu4Loao.js';
|
|
3
|
+
import '../rag-La_Bo-J8.js';
|
|
4
|
+
import '../logger-DxvKliuk.js';
|
|
5
|
+
|
|
6
|
+
interface RetryConfig {
|
|
7
|
+
maxRetries: number;
|
|
8
|
+
baseDelayMs: number;
|
|
9
|
+
}
|
|
10
|
+
interface OpenAICompatibleAdapterConfig {
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
defaultModel?: string;
|
|
14
|
+
defaultHeaders?: Record<string, string>;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
retry?: RetryConfig;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reference implementation of an OpenAI-compatible provider adapter.
|
|
20
|
+
*/
|
|
21
|
+
declare class OpenAICompatibleAdapter implements IProviderAdapter {
|
|
22
|
+
readonly name = "openai_compatible";
|
|
23
|
+
readonly version = "1.0.0";
|
|
24
|
+
private baseUrl;
|
|
25
|
+
private apiKey;
|
|
26
|
+
private defaultModel;
|
|
27
|
+
private defaultHeaders;
|
|
28
|
+
private timeoutMs;
|
|
29
|
+
private retryConfig;
|
|
30
|
+
constructor(config?: OpenAICompatibleAdapterConfig);
|
|
31
|
+
private fetchWithRetry;
|
|
32
|
+
private mapFinishReason;
|
|
33
|
+
private buildPayload;
|
|
34
|
+
complete(request: CompletionRequest): Promise<CompletionResponse>;
|
|
35
|
+
stream(request: CompletionRequest): AsyncIterable<CompletionChunk>;
|
|
36
|
+
estimateTokens(text: string): number;
|
|
37
|
+
getModelInfo(): ModelInfo;
|
|
38
|
+
healthCheck(): Promise<boolean>;
|
|
39
|
+
transcribe(request: TranscriptionRequest): Promise<TranscriptionResponse>;
|
|
40
|
+
synthesize(request: SynthesisRequest): AsyncIterable<AudioChunk>;
|
|
41
|
+
describeImage(request: VisionRequest): Promise<VisionResponse>;
|
|
42
|
+
generateImage(request: ImageGenRequest): Promise<ImageGenResponse>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { OpenAICompatibleAdapter, type OpenAICompatibleAdapterConfig, type RetryConfig };
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/types/errors.ts
|
|
4
|
+
var LemuraError = class extends Error {
|
|
5
|
+
/**
|
|
6
|
+
* @param message - The error message
|
|
7
|
+
* @param code - The error code for programmatic handling
|
|
8
|
+
* @param problem - A clear description of the problem for the end user
|
|
9
|
+
* @param hints - A list of suggestions to resolve the issue
|
|
10
|
+
*/
|
|
11
|
+
constructor(message, code, problem, hints = []) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.problem = problem;
|
|
15
|
+
this.hints = hints;
|
|
16
|
+
this.name = "LemuraError";
|
|
17
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var LemuraAdapterError = class extends LemuraError {
|
|
21
|
+
constructor(message, code = "ADAPTER_ERROR", cause, problem, hints = []) {
|
|
22
|
+
super(message, code, problem, hints);
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
this.name = "LemuraAdapterError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/adapters/OpenAICompatibleAdapter.ts
|
|
29
|
+
var OpenAICompatibleAdapter = class {
|
|
30
|
+
name = "openai_compatible";
|
|
31
|
+
version = "1.0.0";
|
|
32
|
+
baseUrl;
|
|
33
|
+
apiKey;
|
|
34
|
+
defaultModel;
|
|
35
|
+
defaultHeaders;
|
|
36
|
+
timeoutMs;
|
|
37
|
+
retryConfig;
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.baseUrl = (config.baseUrl ?? process.env.LEMURA_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
40
|
+
this.apiKey = config.apiKey ?? process.env.LEMURA_API_KEY ?? process.env.OPENAI_API_KEY ?? "";
|
|
41
|
+
this.defaultModel = config.defaultModel ?? process.env.LEMURA_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-3.5-turbo";
|
|
42
|
+
this.defaultHeaders = config.defaultHeaders || {};
|
|
43
|
+
this.timeoutMs = config.timeout || 3e4;
|
|
44
|
+
this.retryConfig = config.retry || { maxRetries: 2, baseDelayMs: 1e3 };
|
|
45
|
+
}
|
|
46
|
+
async fetchWithRetry(url, init) {
|
|
47
|
+
let attempts = 0;
|
|
48
|
+
while (attempts <= this.retryConfig.maxRetries) {
|
|
49
|
+
try {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
52
|
+
const headers = {
|
|
53
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
54
|
+
...this.defaultHeaders
|
|
55
|
+
};
|
|
56
|
+
if (init.headers) {
|
|
57
|
+
Object.assign(headers, init.headers);
|
|
58
|
+
}
|
|
59
|
+
if (headers["Content-Type"] === "unset") {
|
|
60
|
+
delete headers["Content-Type"];
|
|
61
|
+
} else if (!headers["Content-Type"]) {
|
|
62
|
+
headers["Content-Type"] = "application/json";
|
|
63
|
+
}
|
|
64
|
+
const response = await fetch(url, {
|
|
65
|
+
...init,
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
headers
|
|
68
|
+
});
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
if (response.ok) return response;
|
|
71
|
+
if ((response.status === 429 || response.status === 503) && attempts < this.retryConfig.maxRetries) {
|
|
72
|
+
attempts++;
|
|
73
|
+
const delay = this.retryConfig.baseDelayMs * Math.pow(2, attempts - 1);
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const errorText = await response.text().catch(() => "");
|
|
78
|
+
let problem = "The server replied with an error during the API call.";
|
|
79
|
+
let hints = ["Check the API documentation for the provider you are using."];
|
|
80
|
+
if (response.status === 401) {
|
|
81
|
+
problem = "Authentication failed. The API key is invalid or missing.";
|
|
82
|
+
hints = [
|
|
83
|
+
"Ensure your API key is correctly configured in the adapter or environment variables.",
|
|
84
|
+
"Check if the API key has expired or been revoked."
|
|
85
|
+
];
|
|
86
|
+
} else if (response.status === 404) {
|
|
87
|
+
problem = "The requested resource or model was not found.";
|
|
88
|
+
hints = [
|
|
89
|
+
"Verify that the baseUrl is correct (e.g., https://api.openai.com/v1).",
|
|
90
|
+
"Check if the model name is correct and available for your account.",
|
|
91
|
+
"Ensure you are not appending extra paths to the baseUrl."
|
|
92
|
+
];
|
|
93
|
+
} else if (response.status === 429) {
|
|
94
|
+
problem = "Rate limit exceeded.";
|
|
95
|
+
hints = [
|
|
96
|
+
"Wait a few seconds before retrying.",
|
|
97
|
+
"Check your usage limits and billing status on the provider dashboard."
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
throw new LemuraAdapterError(
|
|
101
|
+
`HTTP ${response.status}: ${errorText}`,
|
|
102
|
+
"HTTP_ERROR",
|
|
103
|
+
{ status: response.status, body: errorText },
|
|
104
|
+
problem,
|
|
105
|
+
hints
|
|
106
|
+
);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err instanceof LemuraAdapterError) throw err;
|
|
109
|
+
if (attempts < this.retryConfig.maxRetries) {
|
|
110
|
+
attempts++;
|
|
111
|
+
const delay = this.retryConfig.baseDelayMs * Math.pow(2, attempts - 1);
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
throw new LemuraAdapterError(
|
|
116
|
+
`Network request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
117
|
+
"NETWORK_ERROR",
|
|
118
|
+
err,
|
|
119
|
+
"A network error occurred while connecting to the provider.",
|
|
120
|
+
[
|
|
121
|
+
"Check your internet connection.",
|
|
122
|
+
"Verify that the baseUrl is reachable from your network.",
|
|
123
|
+
"Check for proxy or firewall settings that might block the request."
|
|
124
|
+
]
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw new LemuraAdapterError(
|
|
129
|
+
"Max retries exceeded",
|
|
130
|
+
"MAX_RETRIES",
|
|
131
|
+
void 0,
|
|
132
|
+
"The request failed after multiple retry attempts.",
|
|
133
|
+
["Check if the provider service is down or experiencing high load."]
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
mapFinishReason(reason) {
|
|
137
|
+
if (!reason) return "stop";
|
|
138
|
+
const r = reason.toLowerCase();
|
|
139
|
+
if (r === "tool_calls" || r === "tool_call") return "tool_call";
|
|
140
|
+
if (r === "length" || r === "max_tokens") return "max_tokens";
|
|
141
|
+
if (r === "content_filter" || r === "error") return "error";
|
|
142
|
+
return "stop";
|
|
143
|
+
}
|
|
144
|
+
buildPayload(request) {
|
|
145
|
+
const payload = {
|
|
146
|
+
model: request.model || this.defaultModel,
|
|
147
|
+
messages: request.messages
|
|
148
|
+
};
|
|
149
|
+
if (request.maxTokens !== void 0) payload.max_tokens = request.maxTokens;
|
|
150
|
+
if (request.temperature !== void 0) payload.temperature = request.temperature;
|
|
151
|
+
if (request.stopSequences?.length) payload.stop = request.stopSequences;
|
|
152
|
+
if (request.stream) payload.stream = true;
|
|
153
|
+
if (request.tools && request.tools.length > 0) {
|
|
154
|
+
payload.tools = request.tools.map((t) => ({
|
|
155
|
+
type: "function",
|
|
156
|
+
function: {
|
|
157
|
+
name: t.name,
|
|
158
|
+
description: t.description,
|
|
159
|
+
parameters: t.parameters
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
return payload;
|
|
164
|
+
}
|
|
165
|
+
async complete(request) {
|
|
166
|
+
const payload = this.buildPayload(request);
|
|
167
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
body: JSON.stringify(payload)
|
|
170
|
+
});
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
const choice = data.choices?.[0];
|
|
173
|
+
if (!choice) {
|
|
174
|
+
throw new LemuraAdapterError("Invalid response format: missing choices", "INVALID_RESPONSE", data);
|
|
175
|
+
}
|
|
176
|
+
const message = choice.message;
|
|
177
|
+
let toolCalls;
|
|
178
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
179
|
+
toolCalls = message.tool_calls.map((tc) => ({
|
|
180
|
+
id: tc.id,
|
|
181
|
+
name: tc.function.name,
|
|
182
|
+
arguments: tc.function.arguments
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
content: message.content || "",
|
|
187
|
+
toolCalls,
|
|
188
|
+
finishReason: this.mapFinishReason(choice.finish_reason),
|
|
189
|
+
usage: {
|
|
190
|
+
promptTokens: data.usage?.prompt_tokens || 0,
|
|
191
|
+
completionTokens: data.usage?.completion_tokens || 0,
|
|
192
|
+
totalTokens: data.usage?.total_tokens || 0
|
|
193
|
+
},
|
|
194
|
+
rawResponse: data
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async *stream(request) {
|
|
198
|
+
const payload = this.buildPayload({ ...request, stream: true });
|
|
199
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
body: JSON.stringify(payload)
|
|
202
|
+
});
|
|
203
|
+
if (!response.body) {
|
|
204
|
+
throw new LemuraAdapterError("Response body is null", "STREAM_ERROR");
|
|
205
|
+
}
|
|
206
|
+
const reader = response.body.getReader();
|
|
207
|
+
const decoder = new TextDecoder("utf-8");
|
|
208
|
+
let buffer = "";
|
|
209
|
+
try {
|
|
210
|
+
while (true) {
|
|
211
|
+
const { value, done } = await reader.read();
|
|
212
|
+
if (done) break;
|
|
213
|
+
buffer += decoder.decode(value, { stream: true });
|
|
214
|
+
const lines = buffer.split("\n");
|
|
215
|
+
buffer = lines.pop() || "";
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
const trimmed = line.trim();
|
|
218
|
+
if (!trimmed || trimmed === "data: [DONE]") continue;
|
|
219
|
+
if (trimmed.startsWith("data: ")) {
|
|
220
|
+
const jsonStr = trimmed.slice(6);
|
|
221
|
+
let data;
|
|
222
|
+
try {
|
|
223
|
+
data = JSON.parse(jsonStr);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const choice = data.choices?.[0];
|
|
228
|
+
if (!choice) continue;
|
|
229
|
+
const delta = choice.delta?.content || "";
|
|
230
|
+
const toolCallBlock = choice.delta?.tool_calls?.[0];
|
|
231
|
+
let toolCallDelta;
|
|
232
|
+
if (toolCallBlock) {
|
|
233
|
+
toolCallDelta = {
|
|
234
|
+
id: toolCallBlock.id,
|
|
235
|
+
name: toolCallBlock.function?.name,
|
|
236
|
+
arguments: toolCallBlock.function?.arguments
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const isFinished = choice.finish_reason !== null && choice.finish_reason !== void 0;
|
|
240
|
+
yield {
|
|
241
|
+
delta,
|
|
242
|
+
finished: isFinished,
|
|
243
|
+
...toolCallDelta && { toolCallDelta },
|
|
244
|
+
...isFinished && { finishReason: this.mapFinishReason(choice.finish_reason) }
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
reader.releaseLock();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
estimateTokens(text) {
|
|
254
|
+
return Math.ceil(text.length / 4);
|
|
255
|
+
}
|
|
256
|
+
getModelInfo() {
|
|
257
|
+
return {
|
|
258
|
+
supportsVision: true,
|
|
259
|
+
supportsTools: true,
|
|
260
|
+
contextWindow: 128e3
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async healthCheck() {
|
|
264
|
+
try {
|
|
265
|
+
const resp = await this.fetchWithRetry(`${this.baseUrl}/models`, { method: "GET" });
|
|
266
|
+
return resp.ok;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async transcribe(request) {
|
|
272
|
+
const binaryString = atob(request.audioBase64);
|
|
273
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
274
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
275
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
276
|
+
}
|
|
277
|
+
const blob = new Blob([bytes], { type: request.mimeType });
|
|
278
|
+
const formData = new FormData();
|
|
279
|
+
formData.append("file", blob, "audio.webm");
|
|
280
|
+
formData.append("model", "whisper-1");
|
|
281
|
+
if (request.language) formData.append("language", request.language);
|
|
282
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/audio/transcriptions`, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: formData,
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "unset"
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const data = await response.json();
|
|
290
|
+
return {
|
|
291
|
+
transcript: data.text,
|
|
292
|
+
confidence: 1,
|
|
293
|
+
// OpenAI doesn't return confidence in standard response
|
|
294
|
+
language: data.language || request.language || "en"
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async *synthesize(request) {
|
|
298
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/audio/speech`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
body: JSON.stringify({
|
|
301
|
+
model: "tts-1",
|
|
302
|
+
input: request.text,
|
|
303
|
+
voice: request.voiceId || "alloy",
|
|
304
|
+
response_format: request.format || "mp3"
|
|
305
|
+
})
|
|
306
|
+
});
|
|
307
|
+
if (!response.body) throw new LemuraAdapterError("No response body for TTS", "STREAM_ERROR");
|
|
308
|
+
const reader = response.body.getReader();
|
|
309
|
+
try {
|
|
310
|
+
while (true) {
|
|
311
|
+
const { done, value } = await reader.read();
|
|
312
|
+
if (done) break;
|
|
313
|
+
if (value) {
|
|
314
|
+
const binary = new TextDecoder("latin1").decode(value);
|
|
315
|
+
yield { audioBase64: btoa(binary) };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} finally {
|
|
319
|
+
reader.releaseLock();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async describeImage(request) {
|
|
323
|
+
const payload = {
|
|
324
|
+
model: this.defaultModel,
|
|
325
|
+
messages: [
|
|
326
|
+
{
|
|
327
|
+
role: "user",
|
|
328
|
+
content: [
|
|
329
|
+
{ type: "text", text: request.prompt || "Describe this image" },
|
|
330
|
+
{
|
|
331
|
+
type: "image_url",
|
|
332
|
+
image_url: {
|
|
333
|
+
url: `data:image/jpeg;base64,${request.imageBase64}`
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
};
|
|
340
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/chat/completions`, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
body: JSON.stringify(payload)
|
|
343
|
+
});
|
|
344
|
+
const data = await response.json();
|
|
345
|
+
return {
|
|
346
|
+
description: data.choices[0].message.content,
|
|
347
|
+
objects: []
|
|
348
|
+
// OpenAI doesn't return structured objects in standard vision call
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async generateImage(request) {
|
|
352
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/images/generations`, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
prompt: request.prompt,
|
|
356
|
+
model: "dall-e-3",
|
|
357
|
+
n: 1,
|
|
358
|
+
size: request.dimensions || "1024x1024"
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
const data = await response.json();
|
|
362
|
+
return {
|
|
363
|
+
imageUrl: data.data[0].url,
|
|
364
|
+
revisedPrompt: data.data[0].revised_prompt
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
exports.OpenAICompatibleAdapter = OpenAICompatibleAdapter;
|
|
370
|
+
//# sourceMappingURL=index.js.map
|
|
371
|
+
//# sourceMappingURL=index.js.map
|