Add initial project files and configurations for clawd_code
This commit is contained in:
@@ -103,6 +103,218 @@ class OpenRouterClient {
|
||||
return ResponseParser.parseOpenRouterResponse(response);
|
||||
}
|
||||
|
||||
Future<ApiMessage> createStreamingMessage({
|
||||
required String model,
|
||||
required int maxTokens,
|
||||
required List<Map<String, dynamic>> messages,
|
||||
String? system,
|
||||
double? temperature,
|
||||
List<Map<String, dynamic>>? tools,
|
||||
String? toolChoice,
|
||||
void Function(String delta)? onTextDelta,
|
||||
}) async {
|
||||
final requestBody = <String, dynamic>{
|
||||
"model": model,
|
||||
"max_tokens": maxTokens,
|
||||
"messages": messages,
|
||||
"stream": true,
|
||||
"stream_options": <String, dynamic>{"include_usage": true},
|
||||
};
|
||||
|
||||
if (system != null) {
|
||||
if (messages.isEmpty || messages.first["role"] != "system") {
|
||||
requestBody["messages"] = [
|
||||
{"role": "system", "content": system},
|
||||
...messages,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (temperature != null) {
|
||||
requestBody["temperature"] = temperature;
|
||||
}
|
||||
|
||||
if (tools != null && tools.isNotEmpty) {
|
||||
requestBody["tools"] = tools;
|
||||
if (toolChoice != null) {
|
||||
requestBody["tool_choice"] = toolChoice;
|
||||
}
|
||||
}
|
||||
|
||||
final url = Uri.parse("$_baseUrl/chat/completions");
|
||||
final headers = _buildHeaders();
|
||||
|
||||
final textBuffer = StringBuffer();
|
||||
final toolCalls = <int, _StreamingToolCallBuilder>{};
|
||||
String responseId = "";
|
||||
String responseModel = model;
|
||||
String? finishReason;
|
||||
Map<String, dynamic>? usage;
|
||||
|
||||
try {
|
||||
if (_requestCancelled) {
|
||||
throw const RequestCancelledException();
|
||||
}
|
||||
|
||||
final request = await _httpClient.openUrl("POST", url);
|
||||
headers.forEach((key, value) {
|
||||
request.headers.set(key, value);
|
||||
});
|
||||
request.headers.contentType = ContentType.json;
|
||||
request.write(jsonEncode(requestBody));
|
||||
|
||||
final response = await request.close();
|
||||
if (response.statusCode >= 400) {
|
||||
final responseBody = await response.transform(utf8.decoder).join();
|
||||
print(
|
||||
"OpenRouter API error ${response.statusCode} for /chat/completions: $responseBody",
|
||||
);
|
||||
_handleErrorResponse(response.statusCode, responseBody);
|
||||
}
|
||||
|
||||
final responseStream = response
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter());
|
||||
|
||||
await for (final line in responseStream) {
|
||||
if (_requestCancelled) {
|
||||
throw const RequestCancelledException();
|
||||
}
|
||||
|
||||
final event = StreamingResponseParser.parseStreamLine(line);
|
||||
if (StreamingResponseParser.isDone(event)) {
|
||||
break;
|
||||
}
|
||||
if (event == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final id = event["id"];
|
||||
if (id is String && id.isNotEmpty) {
|
||||
responseId = id;
|
||||
}
|
||||
|
||||
final streamedModel = event["model"];
|
||||
if (streamedModel is String && streamedModel.isNotEmpty) {
|
||||
responseModel = streamedModel;
|
||||
}
|
||||
|
||||
final rawUsage = event["usage"];
|
||||
if (rawUsage is Map<String, dynamic>) {
|
||||
usage = rawUsage;
|
||||
}
|
||||
|
||||
final choices = event["choices"];
|
||||
if (choices is! List || choices.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final firstChoice = choices.first;
|
||||
if (firstChoice is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final rawFinishReason = firstChoice["finish_reason"];
|
||||
if (rawFinishReason is String && rawFinishReason.isNotEmpty) {
|
||||
finishReason = rawFinishReason == "tool_calls"
|
||||
? "tool_use"
|
||||
: rawFinishReason;
|
||||
}
|
||||
|
||||
final delta = firstChoice["delta"];
|
||||
if (delta is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final content = delta["content"];
|
||||
if (content is String && content.isNotEmpty) {
|
||||
textBuffer.write(content);
|
||||
onTextDelta?.call(content);
|
||||
}
|
||||
|
||||
final toolCallDeltas = delta["tool_calls"];
|
||||
if (toolCallDeltas is! List) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final rawToolCall in toolCallDeltas) {
|
||||
if (rawToolCall is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = (rawToolCall["index"] as num?)?.toInt() ?? 0;
|
||||
final builder = toolCalls.putIfAbsent(
|
||||
index,
|
||||
() => _StreamingToolCallBuilder(),
|
||||
);
|
||||
|
||||
final toolCallId = rawToolCall["id"];
|
||||
if (toolCallId is String && toolCallId.isNotEmpty) {
|
||||
builder.id = toolCallId;
|
||||
}
|
||||
|
||||
final rawType = rawToolCall["type"];
|
||||
if (rawType is String && rawType.isNotEmpty) {
|
||||
builder.type = rawType;
|
||||
}
|
||||
|
||||
final function = rawToolCall["function"];
|
||||
if (function is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final name = function["name"];
|
||||
if (name is String && name.isNotEmpty) {
|
||||
builder.name = name;
|
||||
}
|
||||
|
||||
final arguments = function["arguments"];
|
||||
if (arguments is String && arguments.isNotEmpty) {
|
||||
builder.arguments.write(arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final contentBlocks = <Map<String, dynamic>>[];
|
||||
final text = textBuffer.toString();
|
||||
if (text.isNotEmpty) {
|
||||
contentBlocks.add(<String, dynamic>{"type": "text", "text": text});
|
||||
}
|
||||
|
||||
final orderedToolCalls = toolCalls.entries.toList()
|
||||
..sort((a, b) => a.key.compareTo(b.key));
|
||||
for (final entry in orderedToolCalls) {
|
||||
final builder = entry.value;
|
||||
contentBlocks.add(<String, dynamic>{
|
||||
"type": "tool_use",
|
||||
"id": builder.id,
|
||||
"name": builder.name,
|
||||
"input": builder.parsedArguments,
|
||||
});
|
||||
}
|
||||
|
||||
return ApiMessage(
|
||||
id: responseId,
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: contentBlocks,
|
||||
model: responseModel,
|
||||
stopReason: finishReason,
|
||||
usage: usage,
|
||||
inputTokens: (usage?["prompt_tokens"] as num?)?.toInt(),
|
||||
outputTokens: (usage?["completion_tokens"] as num?)?.toInt(),
|
||||
);
|
||||
} catch (e) {
|
||||
if (_requestCancelled) {
|
||||
throw const RequestCancelledException();
|
||||
}
|
||||
if (_config.enableLogging) {
|
||||
_log("[API STREAM ERROR] $e");
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// List available models
|
||||
Future<List<Map<String, dynamic>>> listModels() async {
|
||||
final response = await _makeRequest(method: "GET", endpoint: "/models");
|
||||
@@ -225,6 +437,27 @@ class OpenRouterClient {
|
||||
}
|
||||
}
|
||||
|
||||
class _StreamingToolCallBuilder {
|
||||
String id = "";
|
||||
String type = "function";
|
||||
String name = "";
|
||||
final StringBuffer arguments = StringBuffer();
|
||||
|
||||
Map<String, dynamic> get parsedArguments {
|
||||
final raw = arguments.toString();
|
||||
if (raw.isEmpty) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
return decoded is Map<String, dynamic> ? decoded : <String, dynamic>{};
|
||||
} catch (_) {
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RequestCancelledException implements Exception {
|
||||
const RequestCancelledException();
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Response parser for Anthropic Message API responses
|
||||
// Ported from old_repo/services/api/errors.ts and claude.ts
|
||||
|
||||
import "dart:convert";
|
||||
|
||||
import "api_types.dart";
|
||||
|
||||
// Parse Message API response into ApiMessage model
|
||||
@@ -145,12 +147,15 @@ class ErrorParser {
|
||||
class StreamingResponseParser {
|
||||
// parse a streamed event from newline-delimited JSON
|
||||
static Map<String, dynamic>? parseStreamLine(String line) {
|
||||
if (line.trim().isEmpty) return null;
|
||||
final trimmed = line.trim();
|
||||
if (trimmed.isEmpty || trimmed.startsWith(":")) return null;
|
||||
|
||||
try {
|
||||
// handle SSE format (data: {...})
|
||||
final data = line.startsWith("data: ") ? line.substring(6) : line;
|
||||
// simple JSON parsing - in production would use json.decode
|
||||
if (data.trim() == "[DONE]") {
|
||||
return <String, dynamic>{"type": "done"};
|
||||
}
|
||||
return _parseJson(data);
|
||||
} catch (_) {
|
||||
return null;
|
||||
@@ -158,9 +163,8 @@ class StreamingResponseParser {
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _parseJson(String jsonStr) {
|
||||
// stubbed - would use dart:convert.jsonDecode in real impl
|
||||
// for now just return null to indicate parsing would happen
|
||||
return null;
|
||||
final decoded = jsonDecode(jsonStr);
|
||||
return decoded is Map<String, dynamic> ? decoded : null;
|
||||
}
|
||||
|
||||
// check if streamed event is a message delta (partial response)
|
||||
@@ -177,6 +181,11 @@ class StreamingResponseParser {
|
||||
return type == "message_stop";
|
||||
}
|
||||
|
||||
static bool isDone(Map<String, dynamic>? event) {
|
||||
if (event == null) return false;
|
||||
return event["type"] == "done";
|
||||
}
|
||||
|
||||
// extract partial text from delta event
|
||||
static String? extractDeltaText(Map<String, dynamic>? event) {
|
||||
if (event == null) return null;
|
||||
|
||||
@@ -13,11 +13,13 @@ class ToolLoopResult {
|
||||
required this.apiMessages,
|
||||
required this.responseText,
|
||||
required this.response,
|
||||
required this.finalResponseWasStreamed,
|
||||
});
|
||||
|
||||
final List<Map<String, dynamic>> apiMessages;
|
||||
final String responseText;
|
||||
final ApiMessage response;
|
||||
final bool finalResponseWasStreamed;
|
||||
}
|
||||
|
||||
class ToolLoopException implements Exception {
|
||||
@@ -48,6 +50,8 @@ class ToolLoopService {
|
||||
String? workingDirectory,
|
||||
void Function(String toolName, Map<String, dynamic> input)? onToolCall,
|
||||
void Function(String toolName, String result)? onToolResult,
|
||||
void Function(String delta)? onAssistantTextDelta,
|
||||
void Function()? onAssistantMessageComplete,
|
||||
}) async {
|
||||
final updatedMessages = List<Map<String, dynamic>>.from(apiMessages)
|
||||
..add(<String, dynamic>{"role": "user", "content": userText});
|
||||
@@ -56,14 +60,22 @@ class ToolLoopService {
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
lastResponse = await client.createMessage(
|
||||
bool streamedTextThisIteration = false;
|
||||
lastResponse = await client.createStreamingMessage(
|
||||
model: model,
|
||||
maxTokens: 4096,
|
||||
messages: updatedMessages,
|
||||
system: _buildSystemPrompt(workingDirectory),
|
||||
tools: _buildToolDefinitions(),
|
||||
toolChoice: "auto",
|
||||
onTextDelta: (delta) {
|
||||
streamedTextThisIteration = true;
|
||||
onAssistantTextDelta?.call(delta);
|
||||
},
|
||||
);
|
||||
if (streamedTextThisIteration) {
|
||||
onAssistantMessageComplete?.call();
|
||||
}
|
||||
|
||||
updatedMessages.add(_assistantMessageForApi(lastResponse));
|
||||
|
||||
@@ -78,6 +90,7 @@ class ToolLoopService {
|
||||
? _buildEmptyAssistantFallback(lastResponse)
|
||||
: responseText,
|
||||
response: lastResponse,
|
||||
finalResponseWasStreamed: streamedTextThisIteration,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,21 @@ class ConversationHistory {
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
void appendToLastMessage(String text) {
|
||||
if (_session == null || _session!.messages.isEmpty || text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final lastMessage = _session!.messages.last;
|
||||
_session!.messages[_session!.messages.length - 1] = Message(
|
||||
role: lastMessage.role,
|
||||
content: "${lastMessage.content}$text",
|
||||
timestamp: lastMessage.timestamp,
|
||||
tokens: lastMessage.tokens,
|
||||
);
|
||||
_session!.updated = DateTime.now().toUtc();
|
||||
}
|
||||
|
||||
void removeLastMessage() {
|
||||
if (_session == null || _session!.messages.isEmpty) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user