Mert Özen Her Satırda Daha İleri
Yapay Zeka Claude API ile Uygulama Geliştirme — #08

Message Batches ve Files API: Toplu İşleri Yarı Fiyatına Çalıştırmak ve Dosyaları Bir Kez Yüklemek

Mert Özen 15 Kas 2026 13 dk 5 görüntülenme
Message Batches ve Files API: Toplu İşleri Yarı Fiyatına Çalıştırmak ve Dosyaları Bir Kez Yüklemek

Acil olmayan binlerce Claude isteğini Message Batches API ile %50 indirimle gönderir, sonuçları sıraya değil custom_id'ye göre eşler ve batch id'sini saklayarak yeniden göndermeyi önlersin; Files API ile dosyayı bir kez yükleyip file_id ile kullanırsın. Dört sonuç tipi, BackgroundService ile gece job'u, 24 saat ve 29 gün sınırları, expires_in_seconds, file_id güvenliği ve document block'unu cache'lemek.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Anthropic C# SDK 13.8.0 ve Claude API version 2026-10-21 ile derlenmiş, birim testleri çalıştırılmıştır. Testler SDK'nın gerçek client'ını ağa çıkmayan sahte bir HttpMessageHandler ile kullanıyor; sahte batch ve dosya cevapları batch processing ve Files API sayfalarındaki örneklerden türetilmiştir. Maliyet satırları yazıdaki aracın fiyat listesiyle yaptığı hesaptır. Limitler, fiyatlar ve davranışlar 15 Kasım 2026'da platform.claude.com belgelerinden alınmıştır. Bu yazıda resmî belgelerden yararlanılmıştır.

Önce Tahmin Et

Selin, sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için katalogdaki 10.000 kitabın her birine bir meta description istiyor. Acelesi yok; sabaha hazır olsun yeter. Emre'nin ilk taslağı bir foreach içinde 10.000 kez Messages.Create çağırıyor. Aynı hafta kurumsal satıştan Hakan, bir yayınevinin 40 MB'lık bahar kataloğu hakkında gün boyunca soru soruyor ve her soruda PDF yeniden base64 olarak gönderiliyor.

Tahminin ne? Gece job'u 6.000'inci kitapta çökerse ve yeniden başlarsa ne olur? Toplu istekler cevap verirken hangi cevabın hangi kitaba ait olduğunu nasıl bilirsin? Ve Hakan'ın PDF'i bir kez yüklenirse, her soruda ödenen input token'ı azalır mı? Bu yazıda Claude API'nin iki indirim aracına bakıyoruz: acil olmayan işler için Message Batches API ve aynı dosyayı tekrar tekrar göndermemek için Files API. Arka plan işinin genel kuralları Background Job'lar ve Scheduled Job'lar serisinde, özellikle retry ve idempotency yazısında anlatıldı.

Message Batches API Nasıl Çalışıyor?

Batch processing sayfasına göre bir batch, her biri normal bir Messages isteği olan istekleri tek seferde gönderdiğin ve asenkron işlenen bir paket. Her istek bağımsız işleniyor. Çoğu batch bir saatten kısa sürede bitiyor; sonuçlar ya bütün istekler bitince ya da 24 saat sonra, hangisi önce gelirse, hazır oluyor. 24 saatte bitmeyen istekler expired oluyor. Bunun karşılığında bütün kullanım standart fiyatın %50'siyle faturalanıyor.

Belgedeki sınırlar: bir batch en fazla 100.000 istek ya da 256 MB, hangisine önce ulaşılırsa. Sonuçlar oluşturulmadan itibaren 29 gün indirilebiliyor. Batch'ler workspace'e bağlı; aynı workspace'teki her istek onları görebiliyor. Batch API'nin kendi rate limit'leri var ve Messages API'nin limitlerini etkilemiyor. Belge bir uyarı daha düşüyor: yüksek paralellik yüzünden bir batch workspace'in harcama limitini az da olsa aşabiliyor.

Batch içinde Messages API'nin neredeyse her özelliği kullanılabiliyor: vision, tool use, server tool'lar, system mesajları, thinking. Dört parametre ise validation hatası veriyor: stream, speed (fast mode), fallbacks ve yedinci yazıdaki pre-warm için kullanılan max_tokens: 0. Bir ayrıntı daha önemli: API, istek içindeki params object'ini asenkron doğruluyor ve hatayı batch bittiğinde bildiriyor. Belge bu yüzden isteğin şeklini önce tek bir Messages çağrısıyla denemeyi öneriyor. API version migration sayfasına göre de batch'teki her istek, batch'in oluşturulduğu API version'ın kurallarıyla işleniyor.

Bookstore: Gece Çalışan SEO Job'u

Her kitap için bir istek üreten kod:

public static class BookSeoBatch
{
    public const string Model = "claude-haiku-5-5";

    // Belgedeki sınır batch başına 100.000 istek ya da 256 MB; küçük batch'leri izlemek ve yeniden göndermek daha kolay.
    public const int MaxRequestsPerBatch = 10_000;

    private const string CustomIdPrefix = "seo-";

    public const string SystemPrompt = """
        You write meta descriptions for the book pages of Bookstore, an online bookstore.
        Write one English description of 140 to 160 characters. Use only the book data you are given.
        Return only the description, without quotes.
        """;

    // custom_id ^[a-zA-Z0-9_-]{1,64}$ ile eşleşmeli. Sonuçlar herhangi bir sırayla gelir ve bu id ile eşlenir.
    public static string CustomIdFor(Book book) => CustomIdPrefix + book.Isbn;

    public static string? IsbnFrom(string customId) =>
        customId.StartsWith(CustomIdPrefix, StringComparison.Ordinal) ? customId[CustomIdPrefix.Length..] : null;

    public static Request BuildRequest(Book book) => new()
    {
        CustomID = CustomIdFor(book),
        Params = new()
        {
            Model = Model,
            MaxTokens = 300,
            System = SystemPrompt,
            // Kısa ve iyi tanımlı bir iş: Haiku 5.5'te thinking gerekmez.
            Thinking = new ThinkingConfigDisabled(),
            Messages = [new UserMessageParam { Content = $"Title: {book.Title}\nAuthor: {book.Author}\nAbout: {book.Description}" }],
        },
    };

    public static IEnumerable<BatchCreateParams> Split(IEnumerable<Book> books) =>
        books.Chunk(MaxRequestsPerBatch)
             .Select(chunk => new BatchCreateParams { Requests = chunk.Select(BuildRequest).ToList() });
}

custom_id belgeye göre 1–64 karakter, yalnız harf, rakam, tire ve alt çizgi. ISBN (International Standard Book Number; kitaplara verilen uluslararası numara) için kullandığımız 13 haneli değer buna uyuyor ve bize bir şey daha veriyor: sonucu hangi kitaba yazacağımızı doğrudan söylüyor. Belge sonuçların sırasının garanti olmadığını açıkça yazıyor; sonucu index'e göre eşlemek, her şeyi yanlış kitaba yazmak demek. Görev kısa ve iyi tanımlı olduğu için Haiku 5.5'te thinking'i kapattık; üçüncü yazıda gördüğümüz gibi bu model thinking'in kapatılmasına izin veriyor. 10.000 kitap belgedeki sınırın çok altında, ama 10.000'lik parçalar halinde göndermek izlemeyi ve yeniden göndermeyi kolaylaştırıyor.

Batch'i gönderen method:

    // Yalnız açıklaması olmayan kitaplar için ve bekleyen bir batch yoksa batch gönderir.
    public async Task<IReadOnlyList<string>> SubmitMissingAsync(CancellationToken cancellationToken)
    {
        if (jobs.Pending().Count > 0)
        {
            return [];
        }

        List<Book> missing = books.All().Where(b => !seo.HasDescription(b.Isbn)).ToList();
        List<string> created = [];
        foreach (BatchCreateParams batch in BookSeoBatch.Split(missing))
        {
            MessageBatch createdBatch = await claude.Messages.Batches.Create(batch, cancellationToken);
            // id'yi her şeyden önce sakla; sonraki çalışma yeni batch göndermek yerine bunu poll eder.
            jobs.Add(createdBatch.ID);
            created.Add(createdBatch.ID);
            logger.LogInformation("SEO batch {BatchId} created with {Count} requests", createdBatch.ID, batch.Requests.Count);
        }

        return created;
    }

İki karar Önce Tahmin Et'teki ilk soruyu cevaplıyor. Birincisi, bekleyen bir batch varsa yenisi gönderilmiyor. İkincisi, batch id'si Create döner dönmez kalıcı bir yere yazılıyor. Job yarıda kesilip yeniden başlarsa yeni bir batch göndermek yerine kayıtlı id'yi poll ediyor. Bu kontrol olmasaydı her restart aynı 10.000 isteği yeniden gönderir ve her birini yeniden öderdi. Belgeye göre gönderilmiş bir batch değiştirilemiyor; değişiklik gerekiyorsa iptal edip yenisini göndermek gerekiyor.

Sonuçları Toplamak: custom_id ve Dört Sonuç Tipi

processing_status önce in_progress, iptal edilirse canceling, iş bitince ended oluyor. Sonuçları SDK'nın ResultsStreaming method'u satır satır okuyor; büyük bir sonuç dosyasını belleğe almıyor:

    // Batch hâlâ çalışıyorsa null döner.
    public async Task<SeoBatchReport?> CollectAsync(string batchId, CancellationToken cancellationToken)
    {
        MessageBatch batch = await claude.Messages.Batches.Retrieve(batchId, cancellationToken: cancellationToken);
        if (batch.ProcessingStatus.Value() != ProcessingStatus.Ended)
        {
            return null;
        }

        int saved = 0;
        List<string> retry = [];
        List<string> rejected = [];

        await foreach (MessageBatchIndividualResponse item in claude.Messages.Batches.ResultsStreaming(batchId, cancellationToken: cancellationToken))
        {
            string? isbn = BookSeoBatch.IsbnFrom(item.CustomID);
            if (isbn is null)
            {
                logger.LogWarning("SEO batch {BatchId}: unknown custom_id {CustomId}", batchId, item.CustomID);
                continue;
            }

            if (item.Result.TryPickSucceeded(out MessageBatchSucceededResult? succeeded))
            {
                if (TryReadDescription(succeeded.Message, out string description))
                {
                    seo.Save(isbn, description);
                    saved++;
                }
                else
                {
                    rejected.Add(isbn);
                }
            }
            else if (item.Result.TryPickErrored(out MessageBatchErroredResult? errored)
                     && errored.Error.Error.Type.Raw() == "invalid_request_error")
            {
                // İsteğin kendisi hatalı; yeniden göndermek aynı hatayı verir.
                logger.LogError("SEO batch {BatchId}: invalid request for {Isbn}", batchId, isbn);
                rejected.Add(isbn);
            }
            else
            {
                // Sunucu hataları, süresi dolan ve iptal edilen istekler faturalanmadı; sonraki batch'e girebilir.
                retry.Add(isbn);
            }
        }

        jobs.Complete(batchId);
        logger.LogInformation("SEO batch {BatchId}: saved={Saved} retry={Retry} rejected={Rejected}",
            batchId, saved, retry.Count, rejected.Count);
        return new SeoBatchReport(batchId, saved, retry, rejected);
    }

Belgedeki dört sonuç tipi ve bizim kararımız:

SonuçBelgeye göreBookstore'da
succeededMesaj sonucu içindestop_reason end_turn ise kaydet
erroredFaturalanmıyor; hata invalid_request_error ya da sunucu hatası olabilirValidation hatası: reddet ve log'la; diğerleri: sonraki batch
canceledBatch iptal edildi, istek gönderilmedi; faturalanmıyorSonraki batch
expired24 saat doldu, istek gönderilmedi; faturalanmıyorSonraki batch

invalid_request_error'ı ayırmamızın sebebi belgede: bu hata isteğin kendisinden geliyor ve aynı isteği yeniden göndermek aynı hatayı veriyor. Ayrıca başarılı bir sonucu da körü körüne kaydetmiyoruz; max_tokens'ta kesilmiş ya da refusal ile dönmüş bir cevap meta description değil.

Testteki sahte sonuç dosyası belgedeki örneklerden türetildi; ikinci kitabın sonucu kasıtlı olarak önce geliyor:

{"custom_id":"seo-9780141439587","result":{"type":"succeeded","message":{"id":"msg_014VwiXbi91y3JMjcpyGBHX5","type":"message","role":"assistant","model":"claude-haiku-5-5","content":[{"type":"text","text":"Emma by Jane Austen: a clever, rich young woman plays matchmaker in her village and misreads almost everyone. A sharp, funny classic."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":96,"output_tokens":34}}}}
{"custom_id":"seo-9780441172719","result":{"type":"succeeded","message":{"id":"msg_01FqfsLoHwgeFbguDgpz48m7","type":"message","role":"assistant","model":"claude-haiku-5-5","content":[{"type":"text","text":"Dune by Frank Herbert: on the desert planet Arrakis, young Paul Atreides is pulled into a war over the spice that rules the galaxy."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":98,"output_tokens":35}}}}
{"custom_id":"seo-9780547928227","result":{"type":"errored","error":{"type":"error","error":{"type":"invalid_request_error","message":"max_tokens: Field required"}}}}
{"custom_id":"seo-9780141439549","result":{"type":"expired"}}

Job'u bir BackgroundService çalıştırıyor:

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (!options.Value.Enabled)
        {
            return;
        }

        using PeriodicTimer timer = new(options.Value.PollInterval);
        do
        {
            try
            {
                using IServiceScope scope = scopes.CreateScope();
                SeoBatchProcessor processor = scope.ServiceProvider.GetRequiredService<SeoBatchProcessor>();
                ISeoBatchJobStore jobs = scope.ServiceProvider.GetRequiredService<ISeoBatchJobStore>();

                foreach (string batchId in jobs.Pending())
                {
                    await processor.CollectAsync(batchId, stoppingToken);
                }

                await processor.SubmitMissingAsync(stoppingToken);
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                logger.LogError(ex, "SEO batch run failed; the next tick tries again");
            }
        }
        while (await timer.WaitForNextTickAsync(stoppingToken));
    }

Worker yapılandırmayla kapalı geliyor, böylece API key'i olmayan bir geliştirme ortamında hiçbir istek gitmiyor. PeriodicTimer aralığı 5 dakika; çoğu batch bir saat içinde bittiği için birkaç saniyede bir sormanın anlamı yok. Her turda önce bekleyen batch'ler toplanıyor, sonra hâlâ açıklaması olmayan kitaplar için yeni batch gönderiliyor. retry listesindeki kitaplar böylece bir sonraki turda kendiliğinden yeni batch'e giriyor. IBookSeoStore burada bellekte; production'da açıklamaları Entity Framework Core ile Books tablosuna yazan bir implementasyon olur. BackgroundService'in yaşam döngüsü ve graceful shutdown Background Job'lar ve Scheduled Job'lar serisinin ikinci yazısında.

Batch, Caching ve Maliyet

Üçüncü yazıdaki değerlendirme aracının estimate komutuna batch sütunu ekledik. Varsayım: istek başına 180 input ve 45 output token.

dotnet run --project tools/Bookstore.ClaudeEval -- estimate 180 45
Assumed tokens per request: input=180 output=45
model                  per request   per 1,000  batch 1,000
claude-fable-5-2          0.003240      3.2400       1.6200
claude-opus-5-5           0.001620      1.6200       0.8100
claude-sonnet-5-5         0.000810      0.8100       0.4050
claude-haiku-5-5          0.000041      0.0405       0.0203

Bu bir ölçüm değil, belgedeki fiyatlarla yapılmış bir hesap. 10.000 kitap Haiku 5.5 ile batch'te yaklaşık 0,20 USD, tek tek gönderildiğinde yaklaşık 0,41 USD tutuyor. Asıl fark büyük modellerde ve uzun prompt'larda ortaya çıkıyor.

Batch indirimi prompt caching çarpanlarıyla birlikte uygulanıyor. Ancak belgeye göre batch'teki istekler paralel ve herhangi bir sırayla işlendiği için cache hit'ler garanti değil, "best effort". Belge, istekler ortak uzun bir context paylaşıyorsa 1 saatlik cache süresini öneriyor; batch 5 dakikadan uzun sürebiliyor. Bizim SEO (arama motoru optimizasyonu) prompt'umuz 512 token'ın altında olduğu için zaten cache'lenmiyor ve breakpoint koymadık.

Files API: Bir Kez Yükle, Çok Kez Kullan

Files API 19 Ağustos 2026'da beta'dan çıktı; artık beta header istemiyor ve C# SDK'da client.Files altında. Mantık basit: dosyayı bir kez yüklüyor, karşılığında bir file_id alıyor ve sonraki isteklerde içeriği yeniden göndermek yerine bu id'ye referans veriyorsun. Yükleme, indirme, listeleme ve silme ücretsiz; dosyanın içeriği bir Messages isteğinde kullanıldığında input token olarak faturalanıyor.

Hakan'ın kataloğunu yükleyen kod:

    // Kataloglar her sezon değişir; dosya 30 gün sonra kendiliğinden silinir (API 1 saat ile 90 gün arasını kabul ediyor).
    public static readonly TimeSpan Retention = TimeSpan.FromDays(30);

    public async Task<PublisherCatalogFile> UploadAsync(
        string publisherId, Stream pdf, string fileName, CancellationToken cancellationToken)
    {
        FileMetadata uploaded = await claude.Files.Upload(new FileUploadParams
        {
            File = new BinaryContent
            {
                Stream = pdf,
                FileName = fileName,
                ContentType = new MediaTypeHeaderValue("application/pdf"),
            },
            ExpiresInSeconds = (long)Retention.TotalSeconds,
        }, cancellationToken);

        PublisherCatalogFile file = new(uploaded.ID, uploaded.Filename, uploaded.ExpiresAt);
        store.Save(publisherId, file);
        logger.LogInformation("Publisher {PublisherId} catalog uploaded as {FileId} ({Bytes} bytes)",
            publisherId, uploaded.ID, uploaded.SizeBytes);
        return file;
    }

Testte SDK'nın gönderdiği multipart gövdesi şöyle görünüyor; expires_in_seconds ayrı bir form alanı:

--7e43f669-5643-416a-95b6-5aac4247d4b8
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=expires_in_seconds

2592000
--7e43f669-5643-416a-95b6-5aac4247d4b8
Content-Type: application/pdf
Content-Disposition: form-data; name=file; filename="spring-catalog.pdf"

%PDF-1.7 sample
--7e43f669-5643-416a-95b6-5aac4247d4b8--

Belgeye göre expires_in_seconds 3.600 (1 saat) ile 7.776.000 (90 gün) arasında olabiliyor. Süresi dolan dosyanın içeriği indirilemiyor, ona referans veren bir Messages isteği daha model çalışmadan hata veriyor, metadata'sı ise 30 gün daha okunabiliyor. Expiry vermezsen dosya sen silene kadar duruyor. Sınırlar: dosya başına 500 MB, organizasyon başına 1 TB. Kendi yüklediğin dosyalar indirilemiyor (downloadable: false); indirilebilenler yalnız skill'lerin ve code execution tool'unun ürettiği dosyalar. Bunu dokuzuncu yazıda kullanacağız.

Dosya tipi block tipini belirliyor: PDF ve düz metin document, resimler image, code execution'a verilecek veri dosyaları container_upload. Yanlış eşleşme, örneğin bir resmi document block'unda göndermek, 400 dönüyor.

Kataloğa soru soran istek:

    public static MessageCreateParams BuildRequest(string fileId, string question, ClaudeOptions settings) => new()
    {
        Model = settings.Model,
        MaxTokens = 2048,
        System = "You answer questions about a publisher's catalog for Bookstore's buyers. Use only the catalog. Say so when the catalog does not answer the question.",
        Messages =
        [
            new UserMessageParam
            {
                Content = new List<ContentBlockParam>
                {
                    // Her soruda aynı doküman: cache'le, dosya her seferinde input token olarak faturalanıyor.
                    new DocumentBlockParam
                    {
                        Source = new FileDocumentSource { FileID = fileId },
                        Title = "Publisher catalog",
                        CacheControl = new CacheControlEphemeral(),
                    },
                    new TextBlockParam { Text = question },
                },
            },
        ],
    };

Önce Tahmin Et'teki son sorunun cevabı burada. file_id HTTP gövdesini küçültüyor ama token maliyetini değiştirmiyor; model dosyayı her soruda yeniden okuyor. Token maliyetini düşüren şey yedinci yazıdaki prompt caching. Bu yüzden document block'una cache_control koyduk: Hakan'ın 5 dakika içindeki ikinci sorusu kataloğu cache'ten okuyor.

file_id Bir Sunucu Referansıdır

Files API sayfasının en önemli uyarısı yetkiyle ilgili. Dosyalar bir son kullanıcıya, konuşmaya ya da session'a değil, workspace'e ait. O workspace'e erişen her API key, oraya yüklenmiş her dosyayı okuyabiliyor. Belge açıkça son kullanıcıdan ya da güvenilmeyen bir kaynaktan file_id kabul etmemeyi söylüyor; aksi hâlde bir kullanıcı başka bir kullanıcının yüklediği içeriği okutabilir.

Bizim tasarımımızda file_id hiç dışarı çıkmıyor. Endpoint yükleme sonrası yalnız dosya adını ve son kullanma tarihini dönüyor; soru endpoint'i publisherId alıyor ve id'yi sunucudaki kayıttan buluyor. LLM Uygulama Güvenliği serisinin dördüncü yazısındaki "yetki kontrolü modelde değil, kodda" ilkesinin bir başka hâli bu. Belge çok kiracılı uygulamalar için her kiracıya ayrı bir workspace öneriyor; organizasyon başına 100 workspace açılabiliyor.

Testler multipart gövdesini, document block'unun JSON'unu ve süresi dolmuş dosyada API'ye hiç gidilmediğini doğruluyor:

Passed!  - Failed:     0, Passed:    43, Skipped:     0, Total:    43, Duration: 62 ms - Bookstore.Api.Tests.dll (net10.0)

Üretimde Dikkat

Batch id'sini kalıcı sakla. Bellek içi store bu yazıdaki örnek için yeterli; production'da bir veritabanı tablosu gerekiyor. Uygulama birden çok instance'ta çalışıyorsa job'u tek bir instance'ın çalıştırmasını sağla; Background Job'lar ve Scheduled Job'lar serisindeki distributed lock konusu burada geçerli.

İsteğin şeklini önce senkron dene. Batch validation hatalarını ancak sonunda öğreniyorsun. Yeni bir prompt ya da parametreyle 10.000'lik batch göndermeden önce aynı isteği bir kez Messages.Create ile gönder.

24 saati hesaba kat. Talep yoğunsa batch daha yavaş işleniyor ve istekler expired olabiliyor. Sabaha yetişmesi gereken bir iş için batch'i akşam erken gönder ve expired olanları ertesi turda yeniden gönder.

Dosyaların ömrünü yönet. Expiry vermediğin dosyalar organizasyonun 1 TB'lık alanında kalıyor. Kataloğun yeni sürümü geldiğinde eskisini client.Files.Delete ile sil; belgeye göre dosyalar değiştirilemiyor, yeni sürüm yeni bir yükleme demek.

Sık Düşülen Tuzaklar

Sonucu index ile eşlemek. Sıra garanti değil; her zaman custom_id kullan.

Yarım kalan job'u yeniden başlatınca yeni batch göndermek. Batch id'sini kaydet, önce bekleyenleri kontrol et.

errored olan her isteği yeniden göndermek. invalid_request_error düzeltilmeden tekrar edilirse yine hata verir.

Batch'te stream ya da speed göndermek. Validation hatası, üstelik batch bitince öğreniliyor.

file_id'yi istemciye vermek ya da istemciden almak. Workspace'teki bütün dosyalar o id'lerle okunabiliyor.

Files API'nin token maliyetini düşürdüğünü sanmak. Düşüren şey prompt caching.

Küçük Bir Deneme

★☆☆ BookSeoBatch.BuildRequest'teki Params'a bir Speed değeri ekle ve testleri çalıştır. Testler bunu yakalıyor mu? Yakalamıyorsa batch'te desteklenmeyen dört parametrenin JSON'da olmadığını doğrulayan bir test yaz ve bu hatayı gerçek API'de ne zaman göreceğini düşün.

★★☆ SeoBatchProcessor'a bir CancelAsync ekle: client.Messages.Batches.Cancel çağırsın, batch canceling'den ended'e geçince CollectAsync ile sonuçları toplasın. Test için belgedeki iptal cevabını fixture olarak kullan.

★★★ PublisherCatalogService'e eski dosyayı temizleyen bir akış ekle: aynı yayınevi için yeni katalog yüklendiğinde eski file_id'yi Files.Delete ile sil ve store'u güncelle. Silme başarısız olursa ne yapacağına karar ver ve bunu bir testle sabitle.

Batch ve Files API, Claude'a gönderdiğin işi ucuzlattı. Dokuzuncu yazıda Claude'un kendi tarafında çalışan yeteneklere geçiyoruz: Skills API ile hazır xlsx, pptx ve pdf skill'leri, code execution tool'u ve remote MCP (Model Context Protocol; modele tool ve veri kaynaklarını standart bir yolla bağlayan protokol) server'ları doğrudan API'den kullanan MCP connector.