Skip to content

Commit 77822ca

Browse files
authored
Merge pull request #273 from RooVetGit/glama
feat: add Glama gateway
2 parents 5e099e2 + 8284efc commit 77822ca

File tree

15 files changed

+771
-8
lines changed

15 files changed

+771
-8
lines changed

.changeset/weak-mugs-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add the Glama provider (thanks @punkpeye!)

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f
1616
- Language selection for Cline's communication (English, Japanese, Spanish, French, German, and more)
1717
- Support for DeepSeek V3
1818
- Support for Amazon Nova and Meta 3, 3.1, and 3.2 models via AWS Bedrock
19+
- Support for Glama
1920
- Support for listing models from OpenAI-compatible providers
2021
- Per-tool MCP auto-approval
2122
- Enable/disable individual MCP servers
@@ -135,7 +136,7 @@ Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.ant
135136

136137
### Use any API and Model
137138

138-
Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
139+
Cline supports API providers like OpenRouter, Anthropic, Glama, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available.
139140

140141
The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way.
141142

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@
214214
"isbinaryfile": "^5.0.2",
215215
"mammoth": "^1.8.0",
216216
"monaco-vscode-textmate-theme-converter": "^0.1.7",
217-
"openai": "^4.61.0",
217+
"openai": "^4.73.1",
218218
"os-name": "^6.0.0",
219219
"p-wait-for": "^5.0.2",
220220
"pdf-parse": "^1.1.1",

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
2+
import { GlamaHandler } from "./providers/glama"
23
import { ApiConfiguration, ModelInfo } from "../shared/api"
34
import { AnthropicHandler } from "./providers/anthropic"
45
import { AwsBedrockHandler } from "./providers/bedrock"
@@ -26,6 +27,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
2627
switch (apiProvider) {
2728
case "anthropic":
2829
return new AnthropicHandler(options)
30+
case "glama":
31+
return new GlamaHandler(options)
2932
case "openrouter":
3033
return new OpenRouterHandler(options)
3134
case "bedrock":

src/api/providers/glama.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
import axios from "axios"
3+
import OpenAI from "openai"
4+
import { ApiHandler } from "../"
5+
import { ApiHandlerOptions, ModelInfo, glamaDefaultModelId, glamaDefaultModelInfo } from "../../shared/api"
6+
import { convertToOpenAiMessages } from "../transform/openai-format"
7+
import { ApiStream } from "../transform/stream"
8+
import delay from "delay"
9+
10+
export class GlamaHandler implements ApiHandler {
11+
private options: ApiHandlerOptions
12+
private client: OpenAI
13+
14+
constructor(options: ApiHandlerOptions) {
15+
this.options = options
16+
this.client = new OpenAI({
17+
baseURL: "https://glama.ai/api/gateway/openai/v1",
18+
apiKey: this.options.glamaApiKey,
19+
})
20+
}
21+
22+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
23+
// Convert Anthropic messages to OpenAI format
24+
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
25+
{ role: "system", content: systemPrompt },
26+
...convertToOpenAiMessages(messages),
27+
]
28+
29+
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
30+
if (this.getModel().id.startsWith("anthropic/claude-3")) {
31+
openAiMessages[0] = {
32+
role: "system",
33+
content: [
34+
{
35+
type: "text",
36+
text: systemPrompt,
37+
// @ts-ignore-next-line
38+
cache_control: { type: "ephemeral" },
39+
},
40+
],
41+
}
42+
43+
// Add cache_control to the last two user messages
44+
// (note: this works because we only ever add one user message at a time,
45+
// but if we added multiple we'd need to mark the user message before the last assistant message)
46+
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
47+
lastTwoUserMessages.forEach((msg) => {
48+
if (typeof msg.content === "string") {
49+
msg.content = [{ type: "text", text: msg.content }]
50+
}
51+
if (Array.isArray(msg.content)) {
52+
// NOTE: this is fine since env details will always be added at the end.
53+
// but if it weren't there, and the user added a image_url type message,
54+
// it would pop a text part before it and then move it after to the end.
55+
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
56+
57+
if (!lastTextPart) {
58+
lastTextPart = { type: "text", text: "..." }
59+
msg.content.push(lastTextPart)
60+
}
61+
// @ts-ignore-next-line
62+
lastTextPart["cache_control"] = { type: "ephemeral" }
63+
}
64+
})
65+
}
66+
67+
// Required by Anthropic
68+
// Other providers default to max tokens allowed.
69+
let maxTokens: number | undefined
70+
71+
if (this.getModel().id.startsWith("anthropic/")) {
72+
maxTokens = 8_192
73+
}
74+
75+
const { data: completion, response } = await this.client.chat.completions.create({
76+
model: this.getModel().id,
77+
max_tokens: maxTokens,
78+
temperature: 0,
79+
messages: openAiMessages,
80+
stream: true,
81+
}).withResponse();
82+
83+
const completionRequestId = response.headers.get(
84+
'x-completion-request-id',
85+
);
86+
87+
for await (const chunk of completion) {
88+
const delta = chunk.choices[0]?.delta
89+
90+
if (delta?.content) {
91+
yield {
92+
type: "text",
93+
text: delta.content,
94+
}
95+
}
96+
}
97+
98+
try {
99+
const response = await axios.get(`https://glama.ai/api/gateway/v1/completion-requests/${completionRequestId}`, {
100+
headers: {
101+
Authorization: `Bearer ${this.options.glamaApiKey}`,
102+
},
103+
})
104+
105+
const completionRequest = response.data;
106+
107+
if (completionRequest.tokenUsage) {
108+
yield {
109+
type: "usage",
110+
inputTokens: completionRequest.tokenUsage.promptTokens,
111+
outputTokens: completionRequest.tokenUsage.completionTokens,
112+
totalCost: completionRequest.totalCostUsd,
113+
}
114+
}
115+
} catch (error) {
116+
// ignore if fails
117+
console.error("Error fetching Glama generation details:", error)
118+
}
119+
}
120+
121+
getModel(): { id: string; info: ModelInfo } {
122+
const modelId = this.options.glamaModelId
123+
const modelInfo = this.options.glamaModelInfo
124+
125+
if (modelId && modelInfo) {
126+
return { id: modelId, info: modelInfo }
127+
}
128+
129+
return { id: glamaDefaultModelId, info: glamaDefaultModelInfo }
130+
}
131+
}

0 commit comments

Comments
 (0)