uttapen

SDK Go

استفاده از پکیج رسمی openai-go با یوتاپن؛ نصب، کلاینت، chat، استریم با accumulator و رویداد uttapen.meta، tool calling، embeddings و مدیریت خطا با.

به‌روزرسانی: ۱۶ شهریور ۱۴۰۵

پکیج رسمی github.com/openai/openai-go (نمونه‌ها با v3 تست شده‌اند) با دو option به uttapen وصل می‌شود. نسخه‌های major مسیر ماژول متفاوتی دارند (/v2، /v3)؛ نمونه‌های این صفحه با v3 هستند و در v1 فقط import و چند نام نوع فرق دارد.

نصب و کلاینت

go get github.com/openai/openai-go/v3
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/option"
)

func main() {
	client := openai.NewClient(
		option.WithBaseURL("https://api.uttapen.ir/v1"),
		option.WithAPIKey(os.Getenv("UTTAPEN_API_KEY")), // sk-up-...
	)
	ctx := context.Background()

	resp, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
		Model: "openai/gpt-5-mini",
		Messages: []openai.ChatCompletionMessageParamUnion{
			openai.SystemMessage("پاسخ کوتاه و فارسی بده."),
			openai.UserMessage("تفاوت goroutine و thread چیست؟"),
		},
		MaxTokens: openai.Int(300),
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.Choices[0].Message.Content)
	fmt.Println(resp.Usage.PromptTokens, resp.Usage.CompletionTokens)
}

option.WithMaxRetries، option.WithRequestTimeout و option.WithHeader (برای X-Uttapen-Include-Meta سراسری) روی سازنده یا هر فراخوانی قابل استفاده‌اند. متغیرهای OPENAI_BASE_URL و OPENAI_API_KEY هم خودکار خوانده می‌شوند.

برای هدر هزینه روی پاسخ غیراستریم:

var raw *http.Response
resp, err := client.Chat.Completions.New(ctx, params, option.WithResponseInto(&raw))
if err == nil {
	fmt.Println(raw.Header.Get("X-Uttapen-Cost-Toman"), raw.Header.Get("X-Uttapen-Balance-Toman"))
}

استریم

stream := client.Chat.Completions.NewStreaming(ctx, openai.ChatCompletionNewParams{
	Model:    "openai/gpt-5-mini",
	Messages: []openai.ChatCompletionMessageParamUnion{openai.UserMessage("یک تابع اعتبارسنجی کد ملی در Go بنویس.")},
}, option.WithHeader("X-Uttapen-Include-Meta", "1"))

acc := openai.ChatCompletionAccumulator{}
for stream.Next() {
	chunk := stream.Current()
	if chunk.Object == "uttapen.meta" {
		fmt.Println("\nmeta:", chunk.RawJSON()) // cost_toman, balance_toman, hold_toman
		continue
	}
	acc.AddChunk(chunk)
	if len(chunk.Choices) > 0 {
		fmt.Print(chunk.Choices[0].Delta.Content)
	}
}
if err := stream.Err(); err != nil {
	panic(err)
}
fmt.Println("\nتوکن خروجی:", acc.Usage.CompletionTokens)

ChatCompletionAccumulator چانک‌ها را به یک ChatCompletion کامل تبدیل می‌کند و برای tool call استریمی آرگومان‌ها را به هم می‌چسباند. رویداد uttapen.meta را قبل از AddChunk جدا کن تا accumulator با چانک بدون choices گیج نشود. لغو استریم با context.WithCancel روی ctx.

Tool calling

tools := []openai.ChatCompletionToolUnionParam{
	openai.ChatCompletionFunctionTool(openai.FunctionDefinitionParam{
		Name:        "get_weather",
		Description: openai.String("دمای فعلی یک شهر"),
		Parameters: openai.FunctionParameters{
			"type":       "object",
			"properties": map[string]any{"city": map[string]any{"type": "string"}},
			"required":   []string{"city"},
		},
	}),
}

msgs := []openai.ChatCompletionMessageParamUnion{openai.UserMessage("هوای تهران؟")}
r1, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{Model: "openai/gpt-5-mini", Messages: msgs, Tools: tools})
if err != nil {
	panic(err)
}
msgs = append(msgs, r1.Choices[0].Message.ToParam())
for _, call := range r1.Choices[0].Message.ToolCalls {
	var args struct{ City string `json:"city"` }
	_ = json.Unmarshal([]byte(call.Function.Arguments), &args)
	result, _ := json.Marshal(map[string]any{"city": args.City, "temp_c": 31})
	msgs = append(msgs, openai.ToolMessage(string(result), call.ID))
}
r2, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{Model: "openai/gpt-5-mini", Messages: msgs, Tools: tools})
if err != nil {
	panic(err)
}
fmt.Println(r2.Choices[0].Message.Content)

Message.ToParam() پیام assistant را با tool_calls به شکل پارامتر ورودی برمی‌گرداند؛ بدون آن provider پیام tool بعدی را رد می‌کند. جزئیات در Tool calling.

Embeddings

emb, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{
	Model: "openai/text-embedding-3-small",
	Input: openai.EmbeddingNewParamsInputUnion{OfArrayOfStrings: []string{"قرارداد اجاره", "اجاره‌نامهٔ ملک مسکونی"}},
})
if err != nil {
	panic(err)
}
fmt.Println(len(emb.Data), len(emb.Data[0].Embedding), emb.Usage.PromptTokens)

برای یک رشته OfString: openai.String("...").

فیلدهای اضافی

فیلدهایی که در struct نیستند را با option.WithJSONSet به بدنه اضافه کن:

resp, err := client.Chat.Completions.New(ctx, params,
	option.WithJSONSet("reasoning", map[string]any{"effort": "low"}),
	option.WithJSONSet("models", []string{"openai/o3-mini"}),
)

مدیریت خطا

import "errors"

_, err := client.Chat.Completions.New(ctx, params)
var apierr *openai.Error
if errors.As(err, &apierr) {
	switch apierr.StatusCode {
	case 401:
		log.Fatal("کلید uttapen نامعتبر است")
	case 402:
		log.Printf("موجودی کافی نیست: %s — %s", apierr.Message, apierr.RawJSON()) // بلوک uttapen در RawJSON
	case 429:
		time.Sleep(5 * time.Second)
	default:
		log.Printf("%d %s: %s", apierr.StatusCode, apierr.Code, apierr.Message)
	}
} else if err != nil {
	log.Printf("خطای شبکه: %v", err)
}

apierr.Code همان error.code ماست، apierr.Request/apierr.Response درخواست و پاسخ خام‌اند و apierr.Response.Header.Get("X-Uttapen-Request-Id") شناسهٔ درخواست. SDK به‌صورت پیش‌فرض دو بار روی 429 و 5xx تکرار می‌کند. جدول کامل در خطاها.

نکته‌ها

  • context را با timeout معقول بساز (مدل‌های استدلالی چند دقیقه طول می‌کشند) و برای استریم context.WithCancel تا با خروج کاربر، درخواست upstream هم لغو شود.
  • در سرورهای HTTP، بعد از هر چانک http.Flusher را صدا بزن و روی nginx خودت proxy_buffering off.
  • مبالغ تومانی رشته‌اند؛ برای جمع کردن از shopspring/decimal استفاده کن، نه float64.

هم‌زمانی و batch

برای پردازش هزاران رکورد، goroutineها را با یک semaphore محدود کن تا از سقف دقیقه‌ای کلید رد نشوی و در 429 با Retry-After صبر کنی. errgroup جمع‌کردن خطاها را ساده می‌کند.

import (
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/semaphore"
)

sem := semaphore.NewWeighted(8) // هشت درخواست هم‌زمان
g, gctx := errgroup.WithContext(ctx)
for _, doc := range docs {
	doc := doc
	g.Go(func() error {
		if err := sem.Acquire(gctx, 1); err != nil {
			return err
		}
		defer sem.Release(1)
		resp, err := client.Chat.Completions.New(gctx, openai.ChatCompletionNewParams{
			Model:     "openai/gpt-5-nano",
			Messages:  []openai.ChatCompletionMessageParamUnion{openai.UserMessage(doc.Text)},
			MaxTokens: openai.Int(200),
		})
		if err != nil {
			return err
		}
		doc.Summary = resp.Choices[0].Message.Content
		return nil
	})
}
if err := g.Wait(); err != nil {
	log.Fatal(err)
}

با ۸ goroutine و پاسخ‌های یک‌ثانیه‌ای حدود ۴۸۰ درخواست در دقیقه می‌زنی که از پیش‌فرض ۶۰ بیشتر است؛ یا rate_limit_rpm کلید را بالا ببر یا یک time.Ticker بین درخواست‌ها بگذار. اولین خطای برگشتی کل گروه را لغو می‌کند؛ اگر می‌خواهی بقیه ادامه دهند، خطا را داخل goroutine ثبت کن و nil برگردان.

استفاده در سرویس HTTP

یک openai.Client برای کل برنامه کافی است؛ thread-safe است و connection pool خودش را دارد. context هر handler را به فراخوانی بده تا با رفتن کلاینت، درخواست به uttapen هم لغو شود و فقط مصرف تا آن لحظه شارژ شود. در shutdown، srv.Shutdown(ctx) همین contextها را می‌بندد و استریم‌های باز تمیز جمع می‌شوند. شناسهٔ X-Uttapen-Request-Id را از option.WithResponseInto بگیر و در لاگ ساخت‌یافتهٔ خودت (slog) کنار شناسهٔ درخواست کاربر بنویس؛ وقتی کاربری شکایت می‌کند، با همین شناسه در چند ثانیه ردیفش را پیدا می‌کنیم.

تست بدون هزینه

منطق خودت را جدا از شبکه تست کن: client را پشت یک interface کوچک (Complete(ctx, prompt) (string, error)) بگذار و در تست، پیاده‌سازی ساختگی بده. برای تست یکپارچه، httptest.NewServer را با یک handler که JSON شبیه پاسخ chat/completions برمی‌گرداند بالا بیاور و option.WithBaseURL(srv.URL + "/v1") بده؛ SDK فرقی بین آن و uttapen نمی‌بیند. برای تست انتها به انتها در CI، یک کلید جدا با allowed_models محدود به openai/gpt-5-nano و سقف ماهانهٔ کوچک بساز؛ هر اجرای تست چند تومان بیشتر خرج نمی‌کند و کلید اصلی در CI نمی‌ماند. option.WithMiddleware هم برای لاگ کردن هر درخواست و پاسخ (بدون بدنه) در یک جا مفید است.