Provider tương thích OpenAI và provider registry
Tổng quan
Chứng minh phần abstraction của phase 3 bằng cách cài adapter thứ hai. Provider tương thích OpenAI tự tổng hợp phần framing block mà Anthropic cung cấp sẵn: BlockStart ở delta tool call đầu tiên mang id và name, argument của tool được gom theo Index, và BlockStop cho mọi block còn mở tại finish_reason. Thêm một provider registry với seam CredentialLookup được tiêm vào, việc chọn provider và model theo từng agent, và kiểm tra capability tại thời điểm lưu agent. Một base URL bao phủ được OpenAI, Ollama, OpenRouter, vLLM và Groq.
Yêu cầu
Chức năng
internal/provider/openaicàiprovider.Providertrên Chat Completions củaopenai-go/v3(không dùng Responses API: hango sở hữu state của session).- Tổng hợp event: gom
Choices[0].Delta.ToolCalls[i]theoIndex; phátBlockStartkhi một tool call lần đầu có đủID+Name,ToolInputDeltacho mỗi mẩu argument,BlockStopcho mọi block còn mở tạifinish_reason; phátMessageStopvới stop reason đã ánh xạ và usage. Không dùngChatCompletionAccumulator(tài liệu ghi là không an toàn với parallel tool call). - Ánh xạ stop reason:
stop→EndTurn,length→MaxTokens,tool_calls→ToolUse,content_filter→Refusal; lỗi độ dài context ánh xạ sangErrContextOverflow. - Thinking block không được hỗ trợ: adapter bỏ chúng khỏi các message gửi đi và báo capability là không có, để một agent bật thinking trên một provider OpenAI thất bại ở bước validation lúc lưu agent chứ không phải giữa chừng một run.
- Registry: `Get(ctx, providerName)` gọi `CredentialLookup` được tiêm vào; ở phase này bản cài duy nhất đọc cấu hình server do operator đặt. Một provider không biết hoặc một provider chưa cấu hình credential trả về một lỗi có kiểu, được hiển thị thành 422 lúc lưu agent và lúc tạo run, không bao giờ là 500 giữa chừng một run.
- `HANGO_OPENAI_BASE_URL` do operator đặt được kiểm tra lúc khởi động: scheme phải là `https`, hoặc `http` chỉ cho host loopback. Giá trị sai làm khởi động thất bại cùng với các lỗi config khác. (Base URL do tenant cung cấp, vốn cần một allowlist SSRF đầy đủ, sẽ đến cùng việc BYO-credential được hoãn.)
- Agent chọn
providervàmodel; validation từ chối một provider không resolve được credential ngay lúc lưu. - `Capabilities` chỉ mang `Thinking` và `PromptCaching`. `PromptCaching` được `PromptCacheMiddleware` (phase 10) dùng, nó bỏ qua việc đặt breakpoint khi giá trị này là false. `ParallelTools` bị xóa: tool batch thực thi tuần tự ở v1 (phase 9), nên sẽ chẳng có gì đọc nó.
Phi chức năng
- Credential chỉ tồn tại bên trong registry lúc dựng client: không bao giờ được log, không bao giờ được endpoint nào trả về, không bao giờ được đặt lên span.
- Registry là toàn cục theo tiến trình và không giữ state theo tenant: nó giữ các client đã dựng được khóa theo danh tính của credential đã resolve, và
CredentialLookupđược hỏi ở mỗi lần resolve. Không có cache khóa theo tenant nào để rò sang tenant khác khi credential BYO xuất hiện. - Adapter được test với các fixture chunk đã ghi lại, gồm parallel tool call với các mẩu argument xen kẽ nhau.
- Chính các test loop đang qua với fake Anthropic phải qua với fake OpenAI mà không đổi loop: sự tương đương đó mới là sản phẩm bàn giao thật sự của phase này.
Kiến trúc
// internal/provider/registry.go
type Registry struct {
creds CredentialLookup // injected; today server config, later a store. Red team: D1
build map[string]Factory
}
type CredentialLookup func(ctx context.Context, providerName string) (Credential, bool, error)
type Credential struct {
APIKey string
BaseURL string
DefaultModel string
}
type Factory func(Credential) (Provider, error)
func (r *Registry) Get(ctx context.Context, name string) (Provider, error) // lookup -> build -> ErrNoProvider
// internal/provider/openai/stream.go — the synthesis core
type blockAccum struct {
index int
id string
name string
argsBuf strings.Builder
started bool
}
// on each chunk: for every ToolCalls[i] delta -> ensure accum by Index, emit BlockStart once id+name known,
// emit ToolInputDelta for Arguments fragments; on finish_reason -> emit BlockStop for all open accums,
// then MessageStop with mapped StopReason and Usage.
// internal/provider/capability.go
type Capabilities struct {
Thinking bool // consumed by agent save-time validation
PromptCaching bool // consumed by PromptCacheMiddleware (phase 10)
// Red team: F15 — ParallelTools deleted: nothing consumes it (batches run sequentially in v1).
}
type CapabilityAware interface{ Capabilities() Capabilities }Instrumentation
- Helper dùng chung
internal/provider/trace.gonghĩa là adapter OpenAI phát ra đúng hình dạng spanchat {model}vớigen_ai.provider.name=openai(vàgen_ai.system=openai), nên mọi dashboard và view Langfuse hoạt động không đổi cho provider thứ hai. Một test khẳng định các attribute giống nhau giữa các adapter cho một stream kịch bản tương đương. - Attribute span riêng của adapter:
hango.provider.base_url_host(chỉ host, không bao giờ có credential), sốhango.provider.synthesized_blocks,hango.provider.parallel_tool_calls. - Span của registry: span INTERNAL
provider.resolvevớigen_ai.provider.namevàhango.provider.credential_source(hiện làserver_config; attribute này có sẵn để việc BYO sau này thêmtenantmà không phải đổi dashboard). - Metric: các metric GenAI hiện có thêm attribute provider (đã có sẵn), cộng counter
hango.provider.resolutions(attributeprovider,source,error.type). - Trường log: việc resolve credential log provider, nguồn và host của base URL — không bao giờ log key hay một URL đầy đủ kèm query string.
Các file liên quan
Tạo mới
internal/provider/openai/provider.go,internal/provider/openai/convert.go,internal/provider/openai/stream.go,internal/provider/openai/stream_test.go,internal/provider/openai/testdata/*.json.internal/provider/registry.go,internal/provider/registry_test.go,internal/provider/capability.go.internal/provider/parity_test.go— chạy cùng một cuộc hội thoại kịch bản qua cả hai adapter, đẩy event vào mộtprovider.Accumulatorcho mỗi adapter, và khẳng định các chuỗi event chuẩn hóa và message đã gom giống hệt nhau (trừ tên provider và thinking).docs/learning/12-second-provider.md.- Không có `internal/auth/crypt.go`, không có credential store, không có credential handler, không có migration.
Sửa đổi
internal/provider/provider.go— ghi tài liệu về hợp đồng tổng hợp cho các adapter không có sẵn framing block native.internal/app/app.go— dựng registry vớiCredentialLookupdựa trên config.internal/run/worker.go— resolve provider cho từng run từ registry dựa trên trườngprovidercủa agent.internal/server/handlers_agents.go— kiểm tra provider và model lúc lưu, gồm cả việc kiểm tra capability thinking.internal/agent/mwcache.go— bỏ qua việc đặt breakpoint khiCapabilities.PromptCachinglà false.internal/config/config.go—HANGO_OPENAI_API_KEY,HANGO_OPENAI_BASE_URL(kiểm tra https-hoặc-loopback lúc khởi động).docs/system-architecture.md,docs/learning/README.md,.env.example.
Các bước triển khai
- Thêm
github.com/openai/openai-go/v3, ghim chính xác một phiên bản (module này ra bản mới gần như hằng ngày). - Viết
internal/provider/capability.go(Thinking,PromptCaching) và càiCapabilityAwaretrên cả hai adapter; nốiPromptCachingvàoPromptCacheMiddleware. - Viết
internal/provider/openai/convert.go: message của hango →ChatCompletionMessageParamUnion(tool result thành các message roletoolkhóa theotool_call_id; system block thành một message system; thinking block bị bỏ kèm một log debug), tool definition của hango →ChatCompletionToolParamvới JSON Schema. - Viết
internal/provider/openai/stream.gocài đặt việc tổng hợp đúng như phác thảo: mộtmap[int]*blockAccum, text delta theo quy ước là index 0,BlockStartđược phát một lần cho mỗi tool call ngay khi có đủ id và name, các mẩu argument được phát dưới dạngToolInputDeltakèm chỉ số block, mọi block còn mở được đóng tạifinish_reason, rồiMessageStop. Không gọiChatCompletionAccumulator. - Ghi fixture cho: chỉ có text, một tool call, hai parallel tool call với các mẩu argument xen kẽ, một tool call mà name đến ở chunk sau id, kết thúc bằng
length, và một lỗi độ dài context. Golden test các chuỗi event được tạo ra và message đã gom. - Viết
internal/provider/registry.govớiCredentialLookupđược tiêm vào (giữproviderkhông phụ thuộcstore), map factory, và các lỗi có kiểuErrNoProvider/ErrProviderMisconfigured. - Nối
internal/appđể dựng registry với lookup dựa trên config, kiểm traHANGO_OPENAI_BASE_URLlà https-hoặc-loopback lúc khởi động. Worker resolve provider cho từng run; resolve thất bại làm run thất bại với một lỗi rõ ràng trước lần gọi model đầu tiên. - Viết
internal/provider/parity_test.go: chạy cùng một cuộc hội thoại kịch bản (text, một tool call, parallel tool call) qua cả hai fake, gom từng cái qua mộtprovider.Accumulator, và khẳng định các chuỗi event chuẩn hóa và message đã gom khớp nhau; sau đó chạy bộ test loop của phase 5 được tham số hóa theo cả hai adapter. - Thêm validation trong agent handler: lưu một agent có provider thiếu credential, hoặc bật thinking trên một provider không có capability, trả về 422 với một thông báo cụ thể.
- Thêm instrumentation và test khớp attribute.
- Cập nhật docs và viết learning doc.
Dàn ý tài liệu học
docs/learning/12-second-provider.md phải có:
- Khái niệm: một abstraction chỉ được chứng minh bởi bản cài đặt thứ hai của nó; “tổng hợp framing block” cụ thể nghĩa là gì.
- Sự bất đối xứng ở mức wire:
content_block_start/input_json_delta/content_block_stopcủa Anthropic so vớiDelta.ToolCalls[]của OpenAI cóIndex, một id và name chỉ xuất hiện ở mẩu đầu tiên, vàArgumentslà các chuỗi JSON một phần. Kèm một bản ghi chunk có chú thích đặt cạnh bản ghi Anthropic của phase 3. - Vì sao
ChatCompletionAccumulator.JustFinishedToolCallkhông an toàn với parallel tool call và hango tự gom theoIndex. - Vì sao dùng Chat Completions mà không dùng Responses API: hango sở hữu state của session, và Chat Completions là thứ các provider tương thích cài đặt.
- Các lựa chọn thiết kế: registry với credential lookup được tiêm vào, credential do operator đặt ở v1, các interface capability cho thinking và caching, validation lúc lưu thay vì thất bại giữa chừng.
- Vì sao BYO credential của tenant được hoãn: hợp đồng yêu cầu API key và quota theo từng tenant, không phải provider key theo từng tenant, và mã hóa cộng rotate khóa cộng một `base_url` của tenant an toàn trước SSRF là một bề mặt bảo mật đáng có một phase riêng. Ghi lại seam giữ được gì và việc tiếp theo phải thêm gì.
- Đánh đổi: seam bây giờ rồi cài đặt sau so với dựng bảng ngay từ đầu; thất bại lúc lưu so với suy giảm lúc chạy.
- Phase 3 đã làm đúng gì và phải uốn chỗ nào: ghi lại trung thực mọi thay đổi mô hình chuẩn hóa mà phase này buộc phải làm.
- So sánh với goclaw:
AdapterRegistryname→factory,Registrykhóa theo"tenantID/name"với fallback về master tenant và mộttenantFromCtxđược tiêm vào để tránh import cycle, cộng bộ máy failover/cooldown/model-fallback mà hango bỏ qua. - So sánh với deepagents: provider abstraction là của LangChain; sự khác biệt OpenAI/Anthropic vô hình với harness — đúng là thuộc tính mà hango phải tự xây bằng tay.
- Cách debug tầng này: các span
provider.resolvecho thấy một run đã dùng nguồn credential nào;hango.provider.resolutionstheosourcevàerror.typebắt lỗi cấu hình sai; vì cả hai adapter phát ra các spanchatgiống hệt nhau, các view Grafana và Langfuse so sánh được các provider cạnh nhau về độ trễ, token và tỉ lệ lỗi. - Bài tập: trỏ adapter OpenAI vào một base URL Ollama cục bộ và chạy cùng một agent; cài một
CredentialLookupgiả trong test và xem registry resolve qua nó. - Ngân sách thời gian: 8h cài đặt + 2h learning doc.
Tiêu chí thành công
-
go build ./... && go vet ./... && go test ./...đều qua. - Golden test bao phủ text, một tool call, hai parallel tool call với các mẩu xen kẽ, tool name đến muộn, kết thúc
length, và lỗi độ dài context. -
parity_test.gocho thấy cùng một chuỗi event chuẩn hóa từ cả hai adapter cho một cuộc hội thoại kịch bản tương đương, và bộ loop của phase 5 qua với cả hai mà không đổi loop. - Test khớp attribute: cả hai adapter phát ra cùng các key attribute của span
chat. -
CredentialLookuplà đường duy nhất tới một credential; một test registry với lookup giả chứng minh registry không bao giờ đọc trực tiếp config hay store. - Lúc khởi động, một
HANGO_OPENAI_BASE_URLkhông phải https và không phải loopback bị từ chối với một lỗi config nêu tên biến. - Lưu một agent với provider không resolve được, hoặc thinking trên một provider không có capability, trả về 422 (test).
- Với một provider báo
PromptCaching: false, không cócache_controlnào xuất hiện trên bất kỳ block nào của request gửi đi (test). - Trong Grafana, các run trên cả hai provider xuất hiện trong cùng các view span
chatvà các metric token/độ trễ tách bạch theogen_ai.provider.name.
Đánh giá rủi ro
| Rủi ro | Khả năng x Tác động | Dấu hiệu | Phản ứng định sẵn |
|---|---|---|---|
| Mô hình chuẩn hóa của phase 3 không diễn đạt được các stream OpenAI | Trung bình x Cao | Việc tổng hợp cần các loại event mới | Index cộng BlockStart/BlockStop tường minh đã được thiết kế cho việc này; nếu vẫn cần thay đổi, nó được làm một lần trong internal/provider và learning doc ghi lại chỗ sót một cách trung thực |
| Argument của parallel tool call xen kẽ bị hỏng | Cao x Cao | Tool được gọi với JSON bị cụt hoặc bị trộn | Gom nghiêm ngặt theo Index, không bao giờ qua ChatCompletionAccumulator; fixture mẩu xen kẽ là cổng chặn |
openai-go/v3 thay đổi liên tục làm hỏng build | Cao x Thấp | Lỗi biên dịch sau một lần nâng cấp | Ghim chính xác phiên bản; adapter là một package và các thay đổi codegen mang tính cơ học |
| Credential rò vào log, span hoặc bản dump config | Trung bình x Nghiêm trọng | Một key hiện ra ở bất kỳ đâu | Secret chỉ tồn tại bên trong registry lúc dựng client; lúc khởi động chỉ log host của base URL; không endpoint nào phơi credential vì ở phase này không có endpoint như vậy |
| BYO credential bị vá vào kém về sau | Trung bình x Cao | Một phase tương lai cố khóa registry theo tenant | Seam CredentialLookup cộng các yêu cầu tường minh của việc tiếp theo (allowlist SSRF, AAD, không cache) được viết ra ngay bây giờ, khi lý lẽ còn mới |
| Agent bật thinking bị chuyển sang OpenAI giữa vòng đời | Trung bình x Trung bình | Run thất bại sau khi đổi provider | Validation capability lúc lưu agent cộng một bước kiểm tra trước run làm run thất bại với một lỗi tương đương 422 trước mọi lời gọi model |
Rollback
Không có migration nào cần hoàn tác. Gỡ factory OpenAI khỏi registry đưa hệ thống về hành vi chỉ-Anthropic mà không đụng vào các agent có provider là anthropic; các agent trỏ vào openai thất bại ở validation với một 422 rõ ràng thay vì thất bại giữa chừng một run.