diff --git a/internal/providers/bailian/bailian.go b/internal/providers/bailian/bailian.go index bd21bf85..9eddead8 100644 --- a/internal/providers/bailian/bailian.go +++ b/internal/providers/bailian/bailian.go @@ -31,37 +31,49 @@ var Registration = providers.Registration{ } // Provider implements the core.Provider interface for Alibaba Cloud Bailian. -// It wraps openai.CompatibleProvider and maps max_tokens to -// max_completion_tokens for every request (Bailian deprecated max_tokens -// in April 2026). +// Transport goes through the shared compatible provider; Bailian's only chat +// quirk is the max_tokens -> max_completion_tokens mapping (Bailian +// deprecated max_tokens in April 2026), applied via the AdaptChatRequest +// hook so every chat-derived path picks it up. Batch and file capabilities +// are exposed through the embedded facet surfaces; the remaining methods are +// delegated explicitly through the compatible provider rather than embedding +// it whole, because Bailian's upstream lacks parts of the full OpenAI +// surface (audio, native /responses) and embedding cannot subtract methods. type Provider struct { + *openai.BatchSurface + *openai.FileSurface compatible *openai.CompatibleProvider apiKey string // retained to inject auth on the realtime websocket target } // New creates a new Bailian provider from a resolved ProviderConfig. func New(cfg providers.ProviderConfig, opts providers.ProviderOptions) core.Provider { - baseURL := providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL) - return &Provider{ - compatible: openai.NewCompatibleProvider(cfg.APIKey, opts, openai.CompatibleProviderConfig{ - ProviderName: "bailian", - BaseURL: baseURL, - SetHeaders: setHeaders, - }), - apiKey: cfg.APIKey, - } + compat := openai.NewCompatibleProvider(cfg.APIKey, opts, compatibleConfig(providers.ResolveBaseURL(cfg.BaseURL, defaultBaseURL))) + return newProvider(compat, cfg.APIKey) } // NewWithHTTPClient creates a new Bailian provider with a custom HTTP client. // If httpClient is nil, http.DefaultClient is used. func NewWithHTTPClient(apiKey string, httpClient *http.Client, hooks llmclient.Hooks) *Provider { + compat := openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, compatibleConfig(defaultBaseURL)) + return newProvider(compat, apiKey) +} + +func newProvider(compat *openai.CompatibleProvider, apiKey string) *Provider { return &Provider{ - compatible: openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, openai.CompatibleProviderConfig{ - ProviderName: "bailian", - BaseURL: defaultBaseURL, - SetHeaders: setHeaders, - }), - apiKey: apiKey, + BatchSurface: openai.NewBatchSurface(compat), + FileSurface: openai.NewFileSurface(compat), + compatible: compat, + apiKey: apiKey, + } +} + +func compatibleConfig(baseURL string) openai.CompatibleProviderConfig { + return openai.CompatibleProviderConfig{ + ProviderName: "bailian", + BaseURL: baseURL, + SetHeaders: setHeaders, + AdaptChatRequest: adaptChatRequest, } } @@ -74,15 +86,14 @@ func (p *Provider) SetBaseURL(url string) { p.compatible.SetBaseURL(url) } -// ChatCompletion maps max_tokens to max_completion_tokens for Bailian models. -// Bailian deprecated max_tokens in April 2026; all models now require max_completion_tokens. +// ChatCompletion sends a chat completion request to Bailian. func (p *Provider) ChatCompletion(ctx context.Context, req *core.ChatRequest) (*core.ChatResponse, error) { - return p.compatible.ChatCompletion(ctx, adaptBailianRequest(req)) + return p.compatible.ChatCompletion(ctx, req) } -// StreamChatCompletion maps max_tokens to max_completion_tokens for streaming requests. +// StreamChatCompletion returns a raw response body for streaming. func (p *Provider) StreamChatCompletion(ctx context.Context, req *core.ChatRequest) (io.ReadCloser, error) { - return p.compatible.StreamChatCompletion(ctx, adaptBailianRequest(req)) + return p.compatible.StreamChatCompletion(ctx, req) } // ListModels returns the list of available models from Bailian. @@ -110,7 +121,7 @@ func (p *Provider) Embeddings(ctx context.Context, req *core.EmbeddingRequest) ( // Passthrough routes an opaque provider-native request to Bailian. // It also adapts max_tokens -> max_completion_tokens in the raw body, -// mirroring the adaptation done in ChatCompletion/StreamChatCompletion. +// mirroring the adaptation done on typed chat requests. func (p *Provider) Passthrough(ctx context.Context, req *core.PassthroughRequest) (*core.PassthroughResponse, error) { if req == nil { return nil, core.NewInvalidRequestError("passthrough request is required", nil) @@ -127,74 +138,7 @@ func (p *Provider) Passthrough(ctx context.Context, req *core.PassthroughRequest return p.compatible.Passthrough(ctx, req) } -// CreateBatch creates a native Bailian batch job. -func (p *Provider) CreateBatch(ctx context.Context, req *core.BatchRequest) (*core.BatchResponse, error) { - return p.compatible.CreateBatch(ctx, req) -} - -// GetBatch retrieves a Bailian batch job by ID. -func (p *Provider) GetBatch(ctx context.Context, id string) (*core.BatchResponse, error) { - return p.compatible.GetBatch(ctx, id) -} - -// ListBatches lists Bailian batch jobs with pagination. -func (p *Provider) ListBatches(ctx context.Context, limit int, after string) (*core.BatchListResponse, error) { - return p.compatible.ListBatches(ctx, limit, after) -} - -// CancelBatch cancels a pending Bailian batch job. -func (p *Provider) CancelBatch(ctx context.Context, id string) (*core.BatchResponse, error) { - return p.compatible.CancelBatch(ctx, id) -} - -// GetBatchResults fetches Bailian batch results via the output file API. -func (p *Provider) GetBatchResults(ctx context.Context, id string) (*core.BatchResultsResponse, error) { - return p.compatible.GetBatchResults(ctx, id) -} - -// CreateFile uploads a file through Bailian's OpenAI-compatible /files API. -func (p *Provider) CreateFile(ctx context.Context, req *core.FileCreateRequest) (*core.FileObject, error) { - resp, err := p.compatible.CreateFile(ctx, req) - if err != nil { - return nil, err - } - resp.Provider = "bailian" - return resp, nil -} - -// ListFiles lists files through Bailian's OpenAI-compatible /files API. -func (p *Provider) ListFiles(ctx context.Context, purpose string, limit int, after string) (*core.FileListResponse, error) { - resp, err := p.compatible.ListFiles(ctx, purpose, limit, after) - if err != nil { - return nil, err - } - for i := range resp.Data { - resp.Data[i].Provider = "bailian" - } - return resp, nil -} - -// GetFile retrieves a file object through Bailian's OpenAI-compatible /files API. -func (p *Provider) GetFile(ctx context.Context, id string) (*core.FileObject, error) { - resp, err := p.compatible.GetFile(ctx, id) - if err != nil { - return nil, err - } - resp.Provider = "bailian" - return resp, nil -} - -// DeleteFile deletes a file through Bailian's OpenAI-compatible /files API. -func (p *Provider) DeleteFile(ctx context.Context, id string) (*core.FileDeleteResponse, error) { - return p.compatible.DeleteFile(ctx, id) -} - -// GetFileContent fetches raw file bytes through Bailian's /files/{id}/content API. -func (p *Provider) GetFileContent(ctx context.Context, id string) (*core.FileContentResponse, error) { - return p.compatible.GetFileContent(ctx, id) -} - -// adaptBailianRequest maps max_tokens -> max_completion_tokens in the request. +// adaptChatRequest maps max_tokens -> max_completion_tokens in the request. // Bailian deprecated max_tokens in April 2026. // It moves MaxTokens into ExtraFields as max_completion_tokens so that when // ChatRequest is serialized, max_tokens is omitted and max_completion_tokens @@ -206,21 +150,21 @@ func (p *Provider) GetFileContent(ctx context.Context, id string) (*core.FileCon // // If either operation fails, the original request is returned unmodified // and a warning is logged so operators can diagnose the issue. -func adaptBailianRequest(req *core.ChatRequest) *core.ChatRequest { +func adaptChatRequest(req *core.ChatRequest) (*core.ChatRequest, error) { if req == nil || req.MaxTokens == nil { - return req + return req, nil } // If the caller already set max_completion_tokens explicitly, respect it. if existing := req.ExtraFields.Lookup("max_completion_tokens"); existing != nil { cloned := *req cloned.MaxTokens = nil - return &cloned + return &cloned, nil } maxTokensJSON, err := json.Marshal(*req.MaxTokens) if err != nil { slog.Warn("bailian: failed to marshal MaxTokens for adaptation, forwarding original request", "error", err) - return req + return req, nil } extra, err := core.MergeUnknownJSONFields(req.ExtraFields, map[string]json.RawMessage{ "max_completion_tokens": maxTokensJSON, @@ -228,12 +172,12 @@ func adaptBailianRequest(req *core.ChatRequest) *core.ChatRequest { if err != nil { slog.Warn("bailian: failed to merge ExtraFields for adaptation, forwarding original request", "error", err) - return req + return req, nil } cloned := *req cloned.ExtraFields = extra cloned.MaxTokens = nil - return &cloned + return &cloned, nil } // adaptPassthroughBody adapts max_tokens -> max_completion_tokens in a raw diff --git a/internal/providers/bailian/bailian_test.go b/internal/providers/bailian/bailian_test.go index 091534a8..26f192bd 100644 --- a/internal/providers/bailian/bailian_test.go +++ b/internal/providers/bailian/bailian_test.go @@ -409,14 +409,21 @@ func TestPassthrough_ReadError(t *testing.T) { } func TestAdaptBailianRequest_Nil(t *testing.T) { - if r := adaptBailianRequest(nil); r != nil { + r, err := adaptChatRequest(nil) + if err != nil { + t.Fatalf("adaptChatRequest(nil) error = %v", err) + } + if r != nil { t.Fatal("expected nil") } } func TestAdaptBailianRequest_NoMaxTokens(t *testing.T) { req := &core.ChatRequest{Model: "qwen3-max"} - r := adaptBailianRequest(req) + r, err := adaptChatRequest(req) + if err != nil { + t.Fatalf("adaptChatRequest() error = %v", err) + } if r.MaxTokens != nil { t.Fatal("should not set max_completion_tokens when request had none") } @@ -474,7 +481,10 @@ func TestAdaptBailianRequest_PreservesOtherFields(t *testing.T) { Messages: []core.Message{{Role: "user", Content: "hi"}}, MaxTokens: &maxTokens, } - r := adaptBailianRequest(req) + r, err := adaptChatRequest(req) + if err != nil { + t.Fatalf("adaptChatRequest() error = %v", err) + } if r == req { t.Fatal("should return a clone, not the original") } @@ -497,7 +507,10 @@ func TestAdaptBailianRequest_RespectsExistingMaxCompletionTokens(t *testing.T) { MaxTokens: &maxTokens, ExtraFields: extra, } - r := adaptBailianRequest(req) + r, err := adaptChatRequest(req) + if err != nil { + t.Fatalf("adaptChatRequest() error = %v", err) + } if r == req { t.Fatal("should return a clone") } diff --git a/internal/providers/groq/groq.go b/internal/providers/groq/groq.go index ca89fac2..a4cbcce1 100644 --- a/internal/providers/groq/groq.go +++ b/internal/providers/groq/groq.go @@ -27,32 +27,42 @@ const ( // Provider implements the core.Provider interface for Groq. Groq's API is // OpenAI-compatible, so all transport goes through the shared compatible -// provider; only the Responses API differs (translated via chat) because the -// gateway does not use Groq's native /responses endpoints. +// provider; the Responses API is translated via chat because the gateway +// does not use Groq's native /responses endpoints. Methods are delegated +// explicitly (and batch/files via facet surfaces) rather than embedding the +// full compatible provider, because Groq's upstream lacks passthrough and +// native response lifecycle management and embedding cannot subtract +// methods. type Provider struct { + *openai.BatchSurface + *openai.FileSurface compat *openai.CompatibleProvider } // New creates a new Groq provider. func New(providerCfg providers.ProviderConfig, opts providers.ProviderOptions) core.Provider { - return &Provider{ - compat: openai.NewCompatibleProvider(providerCfg.APIKey, opts, openai.CompatibleProviderConfig{ - ProviderName: "groq", - BaseURL: providers.ResolveBaseURL(providerCfg.BaseURL, defaultBaseURL), - SetHeaders: setHeaders, - }), - } + return newProvider(openai.NewCompatibleProvider(providerCfg.APIKey, opts, compatibleConfig(providers.ResolveBaseURL(providerCfg.BaseURL, defaultBaseURL)))) } // NewWithHTTPClient creates a new Groq provider with a custom HTTP client. // If httpClient is nil, http.DefaultClient is used. func NewWithHTTPClient(apiKey string, httpClient *http.Client, hooks llmclient.Hooks) *Provider { + return newProvider(openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, compatibleConfig(defaultBaseURL))) +} + +func newProvider(compat *openai.CompatibleProvider) *Provider { return &Provider{ - compat: openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, openai.CompatibleProviderConfig{ - ProviderName: "groq", - BaseURL: defaultBaseURL, - SetHeaders: setHeaders, - }), + BatchSurface: openai.NewBatchSurface(compat), + FileSurface: openai.NewFileSurface(compat), + compat: compat, + } +} + +func compatibleConfig(baseURL string) openai.CompatibleProviderConfig { + return openai.CompatibleProviderConfig{ + ProviderName: "groq", + BaseURL: baseURL, + SetHeaders: setHeaders, } } @@ -109,53 +119,3 @@ func (p *Provider) CreateSpeech(ctx context.Context, req *core.AudioSpeechReques func (p *Provider) CreateTranscription(ctx context.Context, req *core.AudioTranscriptionRequest) (*core.AudioResponse, error) { return p.compat.CreateTranscription(ctx, req) } - -// CreateBatch creates a native Groq batch job. -func (p *Provider) CreateBatch(ctx context.Context, req *core.BatchRequest) (*core.BatchResponse, error) { - return p.compat.CreateBatch(ctx, req) -} - -// GetBatch retrieves a native Groq batch job. -func (p *Provider) GetBatch(ctx context.Context, id string) (*core.BatchResponse, error) { - return p.compat.GetBatch(ctx, id) -} - -// ListBatches lists native Groq batch jobs. -func (p *Provider) ListBatches(ctx context.Context, limit int, after string) (*core.BatchListResponse, error) { - return p.compat.ListBatches(ctx, limit, after) -} - -// CancelBatch cancels a native Groq batch job. -func (p *Provider) CancelBatch(ctx context.Context, id string) (*core.BatchResponse, error) { - return p.compat.CancelBatch(ctx, id) -} - -// GetBatchResults fetches Groq batch results via the output file API. -func (p *Provider) GetBatchResults(ctx context.Context, id string) (*core.BatchResultsResponse, error) { - return p.compat.GetBatchResults(ctx, id) -} - -// CreateFile uploads a file through Groq's OpenAI-compatible /files API. -func (p *Provider) CreateFile(ctx context.Context, req *core.FileCreateRequest) (*core.FileObject, error) { - return p.compat.CreateFile(ctx, req) -} - -// ListFiles lists files through Groq's OpenAI-compatible /files API. -func (p *Provider) ListFiles(ctx context.Context, purpose string, limit int, after string) (*core.FileListResponse, error) { - return p.compat.ListFiles(ctx, purpose, limit, after) -} - -// GetFile retrieves one file object through Groq's OpenAI-compatible /files API. -func (p *Provider) GetFile(ctx context.Context, id string) (*core.FileObject, error) { - return p.compat.GetFile(ctx, id) -} - -// DeleteFile deletes a file object through Groq's OpenAI-compatible /files API. -func (p *Provider) DeleteFile(ctx context.Context, id string) (*core.FileDeleteResponse, error) { - return p.compat.DeleteFile(ctx, id) -} - -// GetFileContent fetches raw file bytes through Groq's /files/{id}/content API. -func (p *Provider) GetFileContent(ctx context.Context, id string) (*core.FileContentResponse, error) { - return p.compat.GetFileContent(ctx, id) -} diff --git a/internal/providers/openai/surfaces.go b/internal/providers/openai/surfaces.go new file mode 100644 index 00000000..69e3cb92 --- /dev/null +++ b/internal/providers/openai/surfaces.go @@ -0,0 +1,87 @@ +package openai + +import ( + "context" + + "gomodel/internal/core" +) + +// BatchSurface is an embeddable facet exposing CompatibleProvider's native +// batch API. Partial-surface providers (see the CompatibleProvider doc) +// embed it to advertise exactly the core.NativeBatchProvider capability +// without inheriting the rest of the full OpenAI surface. +type BatchSurface struct { + compat *CompatibleProvider +} + +// NewBatchSurface wraps compat's batch endpoints as an embeddable facet. +func NewBatchSurface(compat *CompatibleProvider) *BatchSurface { + return &BatchSurface{compat: compat} +} + +var _ core.NativeBatchProvider = (*BatchSurface)(nil) + +// CreateBatch creates a native batch job. +func (s *BatchSurface) CreateBatch(ctx context.Context, req *core.BatchRequest) (*core.BatchResponse, error) { + return s.compat.CreateBatch(ctx, req) +} + +// GetBatch retrieves a native batch job. +func (s *BatchSurface) GetBatch(ctx context.Context, id string) (*core.BatchResponse, error) { + return s.compat.GetBatch(ctx, id) +} + +// ListBatches lists native batch jobs. +func (s *BatchSurface) ListBatches(ctx context.Context, limit int, after string) (*core.BatchListResponse, error) { + return s.compat.ListBatches(ctx, limit, after) +} + +// CancelBatch cancels a native batch job. +func (s *BatchSurface) CancelBatch(ctx context.Context, id string) (*core.BatchResponse, error) { + return s.compat.CancelBatch(ctx, id) +} + +// GetBatchResults fetches batch results via the output file API. +func (s *BatchSurface) GetBatchResults(ctx context.Context, id string) (*core.BatchResultsResponse, error) { + return s.compat.GetBatchResults(ctx, id) +} + +// FileSurface is an embeddable facet exposing CompatibleProvider's +// OpenAI-compatible files API. Partial-surface providers embed it to +// advertise exactly the core.NativeFileProvider capability without +// inheriting the rest of the full OpenAI surface. +type FileSurface struct { + compat *CompatibleProvider +} + +// NewFileSurface wraps compat's file endpoints as an embeddable facet. +func NewFileSurface(compat *CompatibleProvider) *FileSurface { + return &FileSurface{compat: compat} +} + +var _ core.NativeFileProvider = (*FileSurface)(nil) + +// CreateFile uploads a file through the OpenAI-compatible /files API. +func (s *FileSurface) CreateFile(ctx context.Context, req *core.FileCreateRequest) (*core.FileObject, error) { + return s.compat.CreateFile(ctx, req) +} + +// ListFiles lists files through the OpenAI-compatible /files API. +func (s *FileSurface) ListFiles(ctx context.Context, purpose string, limit int, after string) (*core.FileListResponse, error) { + return s.compat.ListFiles(ctx, purpose, limit, after) +} + +// GetFile retrieves one file object through the OpenAI-compatible /files API. +func (s *FileSurface) GetFile(ctx context.Context, id string) (*core.FileObject, error) { + return s.compat.GetFile(ctx, id) +} + +// DeleteFile deletes a file object through the OpenAI-compatible /files API. +func (s *FileSurface) DeleteFile(ctx context.Context, id string) (*core.FileDeleteResponse, error) { + return s.compat.DeleteFile(ctx, id) +} + +// GetFileContent fetches raw file bytes through the /files/{id}/content API. +func (s *FileSurface) GetFileContent(ctx context.Context, id string) (*core.FileContentResponse, error) { + return s.compat.GetFileContent(ctx, id) +} diff --git a/internal/providers/xai/xai.go b/internal/providers/xai/xai.go index 6d8dd36e..93acce70 100644 --- a/internal/providers/xai/xai.go +++ b/internal/providers/xai/xai.go @@ -35,28 +35,34 @@ const ( // OpenAI-compatible, so transport goes through the shared compatible // provider; xAI's only chat quirk is the conversation affinity header // (X-Grok-Conv-Id), injected via the ChatRequestHeaders hook. Methods are -// delegated explicitly rather than embedded because xAI's upstream lacks +// delegated explicitly (and batch/files via facet surfaces) rather than +// embedding the full compatible provider, because xAI's upstream lacks // parts of the full OpenAI surface (audio, passthrough, response // lifecycle management) and embedding cannot subtract methods. type Provider struct { + *openai.BatchSurface + *openai.FileSurface compat *openai.CompatibleProvider apiKey string // retained to inject auth on the realtime websocket target } // New creates a new xAI provider. func New(providerCfg providers.ProviderConfig, opts providers.ProviderOptions) core.Provider { - return &Provider{ - compat: openai.NewCompatibleProvider(providerCfg.APIKey, opts, compatibleConfig(providers.ResolveBaseURL(providerCfg.BaseURL, defaultBaseURL))), - apiKey: providerCfg.APIKey, - } + return newProvider(openai.NewCompatibleProvider(providerCfg.APIKey, opts, compatibleConfig(providers.ResolveBaseURL(providerCfg.BaseURL, defaultBaseURL))), providerCfg.APIKey) } // NewWithHTTPClient creates a new xAI provider with a custom HTTP client. // If httpClient is nil, http.DefaultClient is used. func NewWithHTTPClient(apiKey string, httpClient *http.Client, hooks llmclient.Hooks) *Provider { + return newProvider(openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, compatibleConfig(defaultBaseURL)), apiKey) +} + +func newProvider(compat *openai.CompatibleProvider, apiKey string) *Provider { return &Provider{ - compat: openai.NewCompatibleProviderWithHTTPClient(apiKey, httpClient, hooks, compatibleConfig(defaultBaseURL)), - apiKey: apiKey, + BatchSurface: openai.NewBatchSurface(compat), + FileSurface: openai.NewFileSurface(compat), + compat: compat, + apiKey: apiKey, } } @@ -205,53 +211,3 @@ func (p *Provider) StreamResponses(ctx context.Context, req *core.ResponsesReque func (p *Provider) Embeddings(ctx context.Context, req *core.EmbeddingRequest) (*core.EmbeddingResponse, error) { return p.compat.Embeddings(ctx, req) } - -// CreateBatch creates a native xAI batch job. -func (p *Provider) CreateBatch(ctx context.Context, req *core.BatchRequest) (*core.BatchResponse, error) { - return p.compat.CreateBatch(ctx, req) -} - -// GetBatch retrieves a native xAI batch job. -func (p *Provider) GetBatch(ctx context.Context, id string) (*core.BatchResponse, error) { - return p.compat.GetBatch(ctx, id) -} - -// ListBatches lists native xAI batch jobs. -func (p *Provider) ListBatches(ctx context.Context, limit int, after string) (*core.BatchListResponse, error) { - return p.compat.ListBatches(ctx, limit, after) -} - -// CancelBatch cancels a native xAI batch job. -func (p *Provider) CancelBatch(ctx context.Context, id string) (*core.BatchResponse, error) { - return p.compat.CancelBatch(ctx, id) -} - -// GetBatchResults fetches xAI batch results via the output file API. -func (p *Provider) GetBatchResults(ctx context.Context, id string) (*core.BatchResultsResponse, error) { - return p.compat.GetBatchResults(ctx, id) -} - -// CreateFile uploads a file through xAI's OpenAI-compatible /files API. -func (p *Provider) CreateFile(ctx context.Context, req *core.FileCreateRequest) (*core.FileObject, error) { - return p.compat.CreateFile(ctx, req) -} - -// ListFiles lists files through xAI's OpenAI-compatible /files API. -func (p *Provider) ListFiles(ctx context.Context, purpose string, limit int, after string) (*core.FileListResponse, error) { - return p.compat.ListFiles(ctx, purpose, limit, after) -} - -// GetFile retrieves one file object through xAI's OpenAI-compatible /files API. -func (p *Provider) GetFile(ctx context.Context, id string) (*core.FileObject, error) { - return p.compat.GetFile(ctx, id) -} - -// DeleteFile deletes a file object through xAI's OpenAI-compatible /files API. -func (p *Provider) DeleteFile(ctx context.Context, id string) (*core.FileDeleteResponse, error) { - return p.compat.DeleteFile(ctx, id) -} - -// GetFileContent fetches raw file bytes through xAI's /files/{id}/content API. -func (p *Provider) GetFileContent(ctx context.Context, id string) (*core.FileContentResponse, error) { - return p.compat.GetFileContent(ctx, id) -}