support generic openai api
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gemmatrans-client",
|
"name": "gemmatrans-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -1251,7 +1251,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gemmatrans-client"
|
name = "gemmatrans-client"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gemmatrans-client"
|
name = "gemmatrans-client"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
description = "A translategemma client"
|
description = "A translategemma client"
|
||||||
authors = ["Julian"]
|
authors = ["Julian"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -3,38 +3,61 @@ use serde::{Deserialize, Serialize};
|
|||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
struct Message {
|
||||||
|
role: String,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
struct TranslationPayload {
|
struct TranslationPayload {
|
||||||
model: String,
|
model: String,
|
||||||
prompt: String,
|
messages: Vec<Message>,
|
||||||
stream: bool,
|
stream: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct OllamaResponse {
|
struct OpenAIResponse {
|
||||||
response: Option<String>,
|
choices: Vec<Choice>,
|
||||||
// done: bool,
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Choice {
|
||||||
|
message: Option<Message>,
|
||||||
|
delta: Option<Delta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Delta {
|
||||||
|
content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn translate(
|
async fn translate(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
api_address: String,
|
api_address: String,
|
||||||
|
api_key: String,
|
||||||
payload: TranslationPayload,
|
payload: TranslationPayload,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let url = format!("{}/api/generate", api_address);
|
// Ensure URL doesn't have double slashes if api_address ends with /
|
||||||
|
let base_url = api_address.trim_end_matches('/');
|
||||||
|
let url = format!("{}/chat/completions", base_url);
|
||||||
|
|
||||||
let res = client
|
let mut request = client.post(&url).json(&payload);
|
||||||
.post(&url)
|
|
||||||
.json(&payload)
|
if !api_key.is_empty() {
|
||||||
|
request = request.header("Authorization", format!("Bearer {}", api_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = request
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if !payload.stream {
|
if !payload.stream {
|
||||||
let data = res.json::<OllamaResponse>().await.map_err(|e| e.to_string())?;
|
let data = res.json::<OpenAIResponse>().await.map_err(|e| e.to_string())?;
|
||||||
return Ok(data.response.unwrap_or_default());
|
return Ok(data.choices.get(0).and_then(|c| c.message.as_ref()).map(|m| m.content.clone()).unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stream = res.bytes_stream();
|
let mut stream = res.bytes_stream();
|
||||||
@@ -44,13 +67,21 @@ async fn translate(
|
|||||||
let chunk = item.map_err(|e| e.to_string())?;
|
let chunk = item.map_err(|e| e.to_string())?;
|
||||||
let text = String::from_utf8_lossy(&chunk);
|
let text = String::from_utf8_lossy(&chunk);
|
||||||
|
|
||||||
// Handle potential multiple JSON objects in one chunk
|
|
||||||
for line in text.lines() {
|
for line in text.lines() {
|
||||||
if line.trim().is_empty() { continue; }
|
let line = line.trim();
|
||||||
if let Ok(json) = serde_json::from_str::<OllamaResponse>(line) {
|
if line.is_empty() { continue; }
|
||||||
if let Some(token) = json.response {
|
if line == "data: [DONE]" { break; }
|
||||||
full_response.push_str(&token);
|
|
||||||
app.emit("translation-chunk", token).map_err(|e| e.to_string())?;
|
if let Some(data_str) = line.strip_prefix("data: ") {
|
||||||
|
if let Ok(json) = serde_json::from_str::<OpenAIResponse>(data_str) {
|
||||||
|
if let Some(choice) = json.choices.get(0) {
|
||||||
|
if let Some(delta) = &choice.delta {
|
||||||
|
if let Some(content) = &delta.content {
|
||||||
|
full_response.push_str(content);
|
||||||
|
app.emit("translation-chunk", content).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "gemmatrans-client",
|
"productName": "gemmatrans-client",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"identifier": "top.volan.gemmatrans-client",
|
"identifier": "top.volan.gemmatrans-client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
32
src/App.vue
32
src/App.vue
@@ -181,7 +181,9 @@ const translate = async () => {
|
|||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: settings.modelName,
|
model: settings.modelName,
|
||||||
prompt: prompt,
|
messages: [
|
||||||
|
{ role: "user", content: prompt }
|
||||||
|
],
|
||||||
stream: settings.enableStreaming
|
stream: settings.enableStreaming
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,7 +191,8 @@ const translate = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await invoke<string>('translate', {
|
const response = await invoke<string>('translate', {
|
||||||
apiAddress: settings.ollamaApiAddress,
|
apiAddress: settings.apiBaseUrl,
|
||||||
|
apiKey: settings.apiKey,
|
||||||
payload: requestBody
|
payload: requestBody
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -463,29 +466,38 @@ const translate = async () => {
|
|||||||
<div v-else-if="view === 'settings'" class="flex-1 overflow-y-auto bg-slate-50 dark:bg-slate-950 p-6 md:p-10 min-h-0">
|
<div v-else-if="view === 'settings'" class="flex-1 overflow-y-auto bg-slate-50 dark:bg-slate-950 p-6 md:p-10 min-h-0">
|
||||||
<div class="max-w-2xl mx-auto space-y-8">
|
<div class="max-w-2xl mx-auto space-y-8">
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">API 配置</h2>
|
<h2 class="text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-4">模型接口配置</h2>
|
||||||
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-6 space-y-4">
|
<div class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border dark:border-slate-800 p-6 space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">Ollama API 地址</label>
|
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">API Base URL</label>
|
||||||
<input
|
<input
|
||||||
v-model="settings.ollamaApiAddress"
|
v-model="settings.apiBaseUrl"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full px-4 py-2 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-slate-900 dark:text-slate-100"
|
class="w-full px-4 py-2 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-slate-900 dark:text-slate-100"
|
||||||
placeholder="http://localhost:11434"
|
placeholder="https://api.openai.com/v1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">Ollama 模型名称</label>
|
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">API Key</label>
|
||||||
|
<input
|
||||||
|
v-model="settings.apiKey"
|
||||||
|
type="password"
|
||||||
|
class="w-full px-4 py-2 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-sm text-slate-900 dark:text-slate-100"
|
||||||
|
placeholder="sk-..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">Model</label>
|
||||||
<input
|
<input
|
||||||
v-model="settings.modelName"
|
v-model="settings.modelName"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full px-4 py-2 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-sm text-slate-900 dark:text-slate-100"
|
class="w-full px-4 py-2 border dark:border-slate-700 rounded-lg bg-transparent focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all font-mono text-sm text-slate-900 dark:text-slate-100"
|
||||||
placeholder="translategemma:12b"
|
placeholder="gpt-3.5-turbo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">启用流式输出</label>
|
<label class="text-sm font-medium text-slate-700 dark:text-slate-300">流式输出</label>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-500">在生成时即时渲染文本</p>
|
<p class="text-xs text-slate-500 dark:text-slate-500">在生成时即时渲染文本</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -569,7 +581,7 @@ const translate = async () => {
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="h-8 bg-slate-100 dark:bg-slate-900 border-t dark:border-slate-800 flex items-center px-4 justify-between shrink-0">
|
<footer class="h-8 bg-slate-100 dark:bg-slate-900 border-t dark:border-slate-800 flex items-center px-4 justify-between shrink-0">
|
||||||
<div class="text-[10px] text-slate-400 dark:text-slate-500">
|
<div class="text-[10px] text-slate-400 dark:text-slate-500">
|
||||||
{{ settings.ollamaApiAddress }}
|
{{ settings.apiBaseUrl }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] text-slate-400 dark:text-slate-500">
|
<div class="text-[10px] text-slate-400 dark:text-slate-500">
|
||||||
Client v{{ pkg.version }}
|
Client v{{ pkg.version }}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ Produce only the {TARGET_LANG} translation, without any additional explanations
|
|||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
const isDark = useLocalStorage('is-dark', false);
|
const isDark = useLocalStorage('is-dark', false);
|
||||||
const ollamaApiAddress = useLocalStorage('ollama-api-address', 'http://localhost:11434');
|
const apiBaseUrl = useLocalStorage('api-base-url', 'http://localhost:11434/v1');
|
||||||
|
const apiKey = useLocalStorage('api-key', '');
|
||||||
const modelName = useLocalStorage('model-name', 'translategemma:12b');
|
const modelName = useLocalStorage('model-name', 'translategemma:12b');
|
||||||
const enableStreaming = useLocalStorage('enable-streaming', true);
|
const enableStreaming = useLocalStorage('enable-streaming', true);
|
||||||
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE);
|
const systemPromptTemplate = useLocalStorage('system-prompt-template', DEFAULT_TEMPLATE);
|
||||||
@@ -84,7 +85,8 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isDark,
|
isDark,
|
||||||
ollamaApiAddress,
|
apiBaseUrl,
|
||||||
|
apiKey,
|
||||||
modelName,
|
modelName,
|
||||||
enableStreaming,
|
enableStreaming,
|
||||||
systemPromptTemplate,
|
systemPromptTemplate,
|
||||||
|
|||||||
Reference in New Issue
Block a user